@lossless.org/client 2.2.0 → 2.4.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/nosqldb/classes.doc.d.ts +5 -5
- package/dist_ts/nosqldb/classes.doc.js +1 -1
- package/dist_ts/objectstorage/classes.bucket.d.ts +14 -1
- package/dist_ts/objectstorage/classes.bucket.js +74 -4
- package/dist_ts/objectstorage/classes.objectstreamlengtherror.d.ts +12 -0
- package/dist_ts/objectstorage/classes.objectstreamlengtherror.js +16 -0
- package/dist_ts/objectstorage/classes.smartbucket.js +5 -5
- package/dist_ts/objectstorage/index.d.ts +1 -0
- package/dist_ts/objectstorage/index.js +2 -1
- package/dist_ts/objectstorage/interfaces.d.ts +5 -1
- package/dist_ts/objectstorage/internal.bucketrequests.d.ts +6 -0
- package/dist_ts/objectstorage/internal.bucketrequests.js +12 -3
- package/dist_ts/objectstorage/internal.exactupload.operations.d.ts +8 -1
- package/dist_ts/objectstorage/internal.exactupload.operations.js +3 -3
- package/dist_ts/objectstorage/internal.multipartcopy.d.ts +38 -0
- package/dist_ts/objectstorage/internal.multipartcopy.js +98 -0
- package/dist_ts/objectstorage/plugins.d.ts +2 -1
- package/dist_ts/objectstorage/plugins.js +3 -2
- package/licenses/SOURCES.md +2 -2
- package/package.json +9 -12
- package/readme.md +10 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/nosqldb/classes.doc.ts +5 -5
- package/ts/objectstorage/classes.bucket.ts +78 -3
- package/ts/objectstorage/classes.objectstreamlengtherror.ts +15 -0
- package/ts/objectstorage/classes.smartbucket.ts +5 -5
- package/ts/objectstorage/index.ts +1 -0
- package/ts/objectstorage/interfaces.ts +5 -0
- package/ts/objectstorage/internal.bucketrequests.ts +12 -2
- package/ts/objectstorage/internal.exactupload.operations.ts +4 -3
- package/ts/objectstorage/internal.multipartcopy.ts +169 -0
- package/ts/objectstorage/plugins.ts +2 -1
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -4176,7 +4176,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4176
4176
|
declare public manager: TManager;
|
|
4177
4177
|
|
|
4178
4178
|
/**
|
|
4179
|
-
* Helper to get collection
|
|
4179
|
+
* Helper to get the collection from the instance, falling back to the model's static accessor
|
|
4180
4180
|
*/
|
|
4181
4181
|
private getCollectionSafe(): SmartdataCollection<any> {
|
|
4182
4182
|
return this.collection || (this.constructor as any).collection;
|
|
@@ -6643,14 +6643,14 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
6643
6643
|
|
|
6644
6644
|
/**
|
|
6645
6645
|
* an array of saveable properties of ALL doc
|
|
6646
|
-
* Note: Set by decorators on prototype - NOT declared as instance property
|
|
6646
|
+
* Note: Set by decorators on prototype - NOT declared as instance property, so no class field shadows it
|
|
6647
6647
|
* Declared with definite assignment assertion to satisfy TypeScript without creating instance property
|
|
6648
6648
|
*/
|
|
6649
6649
|
declare globalSaveableProperties: string[];
|
|
6650
6650
|
|
|
6651
6651
|
/**
|
|
6652
6652
|
* unique indexes
|
|
6653
|
-
* Note: Set by decorators on prototype - NOT declared as instance property
|
|
6653
|
+
* Note: Set by decorators on prototype - NOT declared as instance property, so no class field shadows it
|
|
6654
6654
|
*/
|
|
6655
6655
|
declare uniqueIndexes: string[];
|
|
6656
6656
|
|
|
@@ -6659,7 +6659,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
6659
6659
|
|
|
6660
6660
|
/**
|
|
6661
6661
|
* regular indexes with their options
|
|
6662
|
-
* Note: Set by decorators on prototype - NOT declared as instance property
|
|
6662
|
+
* Note: Set by decorators on prototype - NOT declared as instance property, so no class field shadows it
|
|
6663
6663
|
*/
|
|
6664
6664
|
declare regularIndexes: Array<{field: string, options: IIndexOptions}>;
|
|
6665
6665
|
|
|
@@ -6674,7 +6674,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
6674
6674
|
|
|
6675
6675
|
/**
|
|
6676
6676
|
* an array of saveable properties of a specific doc
|
|
6677
|
-
* Note: Set by decorators on prototype - NOT declared as instance property
|
|
6677
|
+
* Note: Set by decorators on prototype - NOT declared as instance property, so no class field shadows it
|
|
6678
6678
|
*/
|
|
6679
6679
|
declare saveableProperties: string[];
|
|
6680
6680
|
|
|
@@ -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,
|
|
@@ -25,6 +29,7 @@ import {
|
|
|
25
29
|
runAccountRequest,
|
|
26
30
|
} from './internal.bucketrequests.js';
|
|
27
31
|
import { ObjectRangeError } from './classes.objectrangeerror.js';
|
|
32
|
+
import { ObjectStreamLengthError } from './classes.objectstreamlengtherror.js';
|
|
28
33
|
|
|
29
34
|
export interface IListObjectKeysPageOptions {
|
|
30
35
|
prefix?: string;
|
|
@@ -253,6 +258,21 @@ const readServedByteRange = (
|
|
|
253
258
|
return { range: { start, end }, size };
|
|
254
259
|
};
|
|
255
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Answers whether the S3 client can measure an upload source of undeclared
|
|
263
|
+
* length itself: it sizes a file stream from its path or descriptor
|
|
264
|
+
* (`calculateBodyLength` of `@smithy/core`), and no other stream. With
|
|
265
|
+
* request checksums on, an unmeasured stream body reaches the wire with an
|
|
266
|
+
* invalid `x-amz-decoded-content-length: undefined` header.
|
|
267
|
+
*/
|
|
268
|
+
const isMeasurableFileStream = (sourceArg: plugins.stream.Readable | ReadableStream): boolean => {
|
|
269
|
+
if (!(sourceArg instanceof plugins.fs.ReadStream)) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
// `fd` is set at runtime but not declared by the Node typings.
|
|
273
|
+
return sourceArg.path != null || typeof (sourceArg as { fd?: unknown }).fd === 'number';
|
|
274
|
+
};
|
|
275
|
+
|
|
256
276
|
const destroyReadableAndWaitForSettlement = async (
|
|
257
277
|
readableArg: plugins.stream.Readable,
|
|
258
278
|
): Promise<void> => {
|
|
@@ -804,7 +824,11 @@ export class Bucket {
|
|
|
804
824
|
}
|
|
805
825
|
|
|
806
826
|
/**
|
|
807
|
-
*
|
|
827
|
+
* Stores a stream with one PutObject, which must declare the body's
|
|
828
|
+
* length: pass `contentLength` unless the source is a file stream
|
|
829
|
+
* (`fs.ReadStream`), which the S3 client measures itself. Any other stream
|
|
830
|
+
* of unknown length is refused with `ObjectStreamLengthError` before any
|
|
831
|
+
* request; it uploads with `fastPutStreamExact()` and `maxContentLength`.
|
|
808
832
|
*/
|
|
809
833
|
public async fastPutStream(
|
|
810
834
|
optionsArg: interfaces.IFastPutStreamOptions,
|
|
@@ -818,6 +842,12 @@ export class Bucket {
|
|
|
818
842
|
) {
|
|
819
843
|
throw new Error('contentLength must be a non-negative safe integer');
|
|
820
844
|
}
|
|
845
|
+
if (optionsArg.contentLength === undefined && !isMeasurableFileStream(optionsArg.readableStream)) {
|
|
846
|
+
throw new ObjectStreamLengthError(
|
|
847
|
+
`fastPutStream for '${optionsArg.path}' needs contentLength: one PutObject cannot upload a ` +
|
|
848
|
+
`stream of unknown length. Pass contentLength, or upload with fastPutStreamExact and maxContentLength.`,
|
|
849
|
+
);
|
|
850
|
+
}
|
|
821
851
|
const operation = beginBasicBucketOperation(this, optionsArg.signal);
|
|
822
852
|
let uploadStream: plugins.stream.Readable | undefined;
|
|
823
853
|
let sourceOwnershipStarted = false;
|
|
@@ -930,8 +960,18 @@ export class Bucket {
|
|
|
930
960
|
return purgeExactPathOperation(this, optionsArg);
|
|
931
961
|
}
|
|
932
962
|
|
|
963
|
+
/**
|
|
964
|
+
* Copies an object within the storage account. Sources up to 5 GiB are
|
|
965
|
+
* copied with one CopyObject; larger sources, which CopyObject refuses, are
|
|
966
|
+
* copied part by part with UploadPartCopy, each part conditional on the
|
|
967
|
+
* source ETag read first.
|
|
968
|
+
* A failed or aborted part copy aborts the multipart upload. Object tags,
|
|
969
|
+
* storage class, server-side encryption settings and a website redirect are
|
|
970
|
+
* not carried by a part-by-part copy.
|
|
971
|
+
*/
|
|
933
972
|
public async fastCopy(optionsArg: interfaces.IFastCopyOptions): Promise<void> {
|
|
934
973
|
const operation = beginBasicBucketOperation(this, optionsArg.signal);
|
|
974
|
+
const deferredCleanups: Promise<void>[] = [];
|
|
935
975
|
try {
|
|
936
976
|
const targetBucketName = optionsArg.targetBucket ? optionsArg.targetBucket.name : this.name;
|
|
937
977
|
|
|
@@ -951,10 +991,41 @@ export class Bucket {
|
|
|
951
991
|
|
|
952
992
|
// Define the copy operation
|
|
953
993
|
const copySource = `${operation.bucketName}/${optionsArg.sourcePath}`;
|
|
994
|
+
const targetKey = optionsArg.destinationPath || optionsArg.sourcePath;
|
|
995
|
+
const sourceSize = currentObjInfo.ContentLength;
|
|
996
|
+
if (typeof sourceSize === 'number' && sourceSize > multipartCopyThresholdBytes) {
|
|
997
|
+
if (typeof currentObjInfo.ETag !== 'string' || currentObjInfo.ETag.length === 0) {
|
|
998
|
+
throw new Error('HeadObject returned no ETag for the copy source.');
|
|
999
|
+
}
|
|
1000
|
+
await copyObjectMultipart(operation, {
|
|
1001
|
+
copySource,
|
|
1002
|
+
sourceSize,
|
|
1003
|
+
sourceEtag: currentObjInfo.ETag,
|
|
1004
|
+
targetBucketName,
|
|
1005
|
+
targetKey,
|
|
1006
|
+
// Mirrors CopyObject: COPY keeps the source metadata and content
|
|
1007
|
+
// headers, REPLACE sets only the given metadata.
|
|
1008
|
+
objectProperties: optionsArg.deleteExistingNativeMetadata
|
|
1009
|
+
? { Metadata: newNativeMetadata }
|
|
1010
|
+
: {
|
|
1011
|
+
Metadata: currentObjInfo.Metadata,
|
|
1012
|
+
ContentType: currentObjInfo.ContentType,
|
|
1013
|
+
CacheControl: currentObjInfo.CacheControl,
|
|
1014
|
+
ContentDisposition: currentObjInfo.ContentDisposition,
|
|
1015
|
+
ContentEncoding: currentObjInfo.ContentEncoding,
|
|
1016
|
+
ContentLanguage: currentObjInfo.ContentLanguage,
|
|
1017
|
+
Expires: currentObjInfo.Expires,
|
|
1018
|
+
},
|
|
1019
|
+
onDeferredCleanup: (settlementArg) => {
|
|
1020
|
+
deferredCleanups.push(settlementArg);
|
|
1021
|
+
},
|
|
1022
|
+
});
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
954
1025
|
const command = new plugins.s3.CopyObjectCommand({
|
|
955
1026
|
Bucket: targetBucketName,
|
|
956
1027
|
CopySource: copySource,
|
|
957
|
-
Key:
|
|
1028
|
+
Key: targetKey,
|
|
958
1029
|
Metadata: newNativeMetadata,
|
|
959
1030
|
MetadataDirective: optionsArg.deleteExistingNativeMetadata ? 'REPLACE' : 'COPY',
|
|
960
1031
|
});
|
|
@@ -962,7 +1033,11 @@ export class Bucket {
|
|
|
962
1033
|
() => operation.client.send(command, { abortSignal: operation.signal }),
|
|
963
1034
|
);
|
|
964
1035
|
} finally {
|
|
965
|
-
|
|
1036
|
+
if (deferredCleanups.length > 0) {
|
|
1037
|
+
void Promise.allSettled(deferredCleanups).then(() => operation.finish());
|
|
1038
|
+
} else {
|
|
1039
|
+
operation.finish();
|
|
1040
|
+
}
|
|
966
1041
|
}
|
|
967
1042
|
}
|
|
968
1043
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The refusal of a single-request stream upload whose length is not known.
|
|
3
|
+
* One PutObject must declare the length of its body, and the S3 client
|
|
4
|
+
* measures only a file stream (`fs.ReadStream`) itself, so any other stream
|
|
5
|
+
* needs `contentLength`; a stream of unknown length uploads with
|
|
6
|
+
* `fastPutStreamExact()` and `maxContentLength`. It is thrown before any
|
|
7
|
+
* request is sent and before the source is read; the caller still owns the
|
|
8
|
+
* source.
|
|
9
|
+
*/
|
|
10
|
+
export class ObjectStreamLengthError extends Error {
|
|
11
|
+
constructor(messageArg: string) {
|
|
12
|
+
super(messageArg);
|
|
13
|
+
this.name = 'ObjectStreamLengthError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import * as plugins from './plugins.js';
|
|
4
4
|
import { Bucket } from './classes.bucket.js';
|
|
5
5
|
import { normalizeStorageDescriptor } from './helpers.js';
|
|
6
|
-
import { headBucketExists } from './internal.bucketrequests.js';
|
|
6
|
+
import { headBucketExists, isMissingBucketError } from './internal.bucketrequests.js';
|
|
7
7
|
import {
|
|
8
8
|
bindRequestDeadline,
|
|
9
9
|
createBoundedClientConfig,
|
|
@@ -28,7 +28,7 @@ import type {
|
|
|
28
28
|
|
|
29
29
|
type TSmartBucketLifecycleState = 'open' | 'closing' | 'closed';
|
|
30
30
|
type TSmartBucketReadinessAbortSource = 'caller' | 'deadline';
|
|
31
|
-
type TSmartBucketReadinessSendOutcome = 'ready' | 'aborted' | 'unavailable';
|
|
31
|
+
type TSmartBucketReadinessSendOutcome = 'ready' | 'aborted' | 'bucket-missing' | 'unavailable';
|
|
32
32
|
|
|
33
33
|
const readinessDefaultTimeoutMs = 2_000;
|
|
34
34
|
const readinessMaximumTimeoutMs = 30_000;
|
|
@@ -314,8 +314,8 @@ export class SmartBucket {
|
|
|
314
314
|
}
|
|
315
315
|
return this.createReadinessFailure('unavailable', startedAt);
|
|
316
316
|
}
|
|
317
|
-
if (sendOutcome === 'unavailable') {
|
|
318
|
-
return this.createReadinessFailure(
|
|
317
|
+
if (sendOutcome === 'bucket-missing' || sendOutcome === 'unavailable') {
|
|
318
|
+
return this.createReadinessFailure(sendOutcome, startedAt);
|
|
319
319
|
}
|
|
320
320
|
|
|
321
321
|
return {
|
|
@@ -432,7 +432,7 @@ export class SmartBucket {
|
|
|
432
432
|
);
|
|
433
433
|
void providerPromise.then(
|
|
434
434
|
() => settle('ready'),
|
|
435
|
-
() => settle('unavailable'),
|
|
435
|
+
(errorArg: unknown) => settle(isMissingBucketError(errorArg) ? 'bucket-missing' : 'unavailable'),
|
|
436
436
|
);
|
|
437
437
|
} catch {
|
|
438
438
|
settle('unavailable');
|
|
@@ -3,6 +3,7 @@ export * from './classes.bucket.js';
|
|
|
3
3
|
export * from './classes.exactuploaderror.js';
|
|
4
4
|
export * from './classes.exactpathpurgeerror.js';
|
|
5
5
|
export * from './classes.objectrangeerror.js';
|
|
6
|
+
export * from './classes.objectstreamlengtherror.js';
|
|
6
7
|
export * from './classes.requestdeadlineerror.js';
|
|
7
8
|
export * from './classes.directory.js';
|
|
8
9
|
export * from './classes.file.js';
|
|
@@ -177,12 +177,17 @@ export interface ISmartBucketReadinessOptions {
|
|
|
177
177
|
|
|
178
178
|
/**
|
|
179
179
|
* Sanitized reasons why a SmartBucket readiness probe is unavailable.
|
|
180
|
+
* `bucket-missing` means the provider answered the probe definitively that
|
|
181
|
+
* the bucket does not exist (`NoSuchBucket`, or HTTP 404). A denied probe
|
|
182
|
+
* (HTTP 403) says nothing about existence and stays `unavailable`, as do
|
|
183
|
+
* other provider and transport failures.
|
|
180
184
|
*/
|
|
181
185
|
export type TSmartBucketReadinessFailureReason =
|
|
182
186
|
| 'closing'
|
|
183
187
|
| 'closed'
|
|
184
188
|
| 'aborted'
|
|
185
189
|
| 'timeout'
|
|
190
|
+
| 'bucket-missing'
|
|
186
191
|
| 'unavailable';
|
|
187
192
|
|
|
188
193
|
/**
|
|
@@ -49,6 +49,16 @@ export const isMissingObjectError = (errorArg: unknown): boolean => {
|
|
|
49
49
|
return name === 'NoSuchKey' || name === 'NotFound' || httpStatusCode === 404;
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Answers whether a provider error says the addressed bucket does not exist:
|
|
54
|
+
* `NoSuchBucket`, HeadBucket's body-less `NotFound`, or any other HTTP 404.
|
|
55
|
+
* A denied request (HTTP 403) says nothing about existence and is not one.
|
|
56
|
+
*/
|
|
57
|
+
export const isMissingBucketError = (errorArg: unknown): boolean => {
|
|
58
|
+
const { name, httpStatusCode } = getProviderStatus(errorArg);
|
|
59
|
+
return name === 'NoSuchBucket' || name === 'NotFound' || httpStatusCode === 404;
|
|
60
|
+
};
|
|
61
|
+
|
|
52
62
|
/**
|
|
53
63
|
* Answers whether the bucket exists with one HeadBucket request, which a
|
|
54
64
|
* bucket-scoped credential may send for its own bucket. A missing bucket
|
|
@@ -68,10 +78,10 @@ export const headBucketExists = async (
|
|
|
68
78
|
);
|
|
69
79
|
return true;
|
|
70
80
|
} catch (error) {
|
|
71
|
-
|
|
72
|
-
if (httpStatusCode === 404 || name === 'NotFound' || name === 'NoSuchBucket') {
|
|
81
|
+
if (isMissingBucketError(error)) {
|
|
73
82
|
return false;
|
|
74
83
|
}
|
|
84
|
+
const { name, httpStatusCode } = getProviderStatus(error);
|
|
75
85
|
if (httpStatusCode === 403 || name === 'Forbidden' || name === 'AccessDenied') {
|
|
76
86
|
throw new Error(
|
|
77
87
|
`Access to bucket '${bucketNameArg}' was denied (HTTP 403): the credentials may not ` +
|
|
@@ -47,7 +47,7 @@ interface IIndependentCleanupResult {
|
|
|
47
47
|
settlement: Promise<void>;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
interface IMultipartAbortResult {
|
|
50
|
+
export interface IMultipartAbortResult {
|
|
51
51
|
cleanup: TExactUploadMultipartCleanup;
|
|
52
52
|
timedOut: boolean;
|
|
53
53
|
settlement: Promise<void>;
|
|
@@ -1266,10 +1266,11 @@ export const probeExactUploadCapability = async (
|
|
|
1266
1266
|
}
|
|
1267
1267
|
};
|
|
1268
1268
|
|
|
1269
|
-
const abortMultipartUpload = async (
|
|
1269
|
+
export const abortMultipartUpload = async (
|
|
1270
1270
|
operationArg: IExactUploadOperation,
|
|
1271
1271
|
keyArg: string,
|
|
1272
1272
|
uploadIdArg: string,
|
|
1273
|
+
bucketNameArg = operationArg.bucketName,
|
|
1273
1274
|
): Promise<IMultipartAbortResult> => {
|
|
1274
1275
|
let reconciliationResult: 'aborted' | 'absent' | undefined;
|
|
1275
1276
|
const cleanupResult = await runIndependentCleanup(
|
|
@@ -1277,7 +1278,7 @@ const abortMultipartUpload = async (
|
|
|
1277
1278
|
async (signalArg) => {
|
|
1278
1279
|
reconciliationResult = await reconcileMultipartUploadAbort(
|
|
1279
1280
|
operationArg.client,
|
|
1280
|
-
|
|
1281
|
+
bucketNameArg,
|
|
1281
1282
|
keyArg,
|
|
1282
1283
|
uploadIdArg,
|
|
1283
1284
|
signalArg,
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type { IBasicBucketOperation } from './internal.exactupload.capability.js';
|
|
3
|
+
import { abortMultipartUpload } from './internal.exactupload.operations.js';
|
|
4
|
+
|
|
5
|
+
const MiB = 1024 * 1024;
|
|
6
|
+
const GiB = 1024 * MiB;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sources larger than this are copied with UploadPartCopy. It is the largest
|
|
10
|
+
* object one CopyObject may copy, so every copy that one CopyObject can do
|
|
11
|
+
* stays one. A capped MinIO (2 CPUs, 2 GiB) answered a single CopyObject after
|
|
12
|
+
* about 3 s per GiB, inside the default 60 s request timeout at this size.
|
|
13
|
+
*/
|
|
14
|
+
export const multipartCopyThresholdBytes = 5 * GiB;
|
|
15
|
+
/** One 256 MiB UploadPartCopy answered within 0.9 s on the same MinIO. */
|
|
16
|
+
export const multipartCopyPartSizeBytes = 256 * MiB;
|
|
17
|
+
const maximumCopyPartSizeBytes = 5 * GiB;
|
|
18
|
+
const maximumCopyPartCount = 10_000;
|
|
19
|
+
const maximumCopyObjectSizeBytes = 5 * 1024 * GiB;
|
|
20
|
+
|
|
21
|
+
export interface IMultipartCopyPlan {
|
|
22
|
+
partSize: number;
|
|
23
|
+
partCount: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Grows the part size so the object fits in 10,000 parts and refuses objects
|
|
28
|
+
* that S3 cannot hold.
|
|
29
|
+
*/
|
|
30
|
+
export const planMultipartCopy = (
|
|
31
|
+
objectSizeArg: number,
|
|
32
|
+
partSizeArg = multipartCopyPartSizeBytes,
|
|
33
|
+
): IMultipartCopyPlan => {
|
|
34
|
+
if (
|
|
35
|
+
!Number.isSafeInteger(objectSizeArg) ||
|
|
36
|
+
objectSizeArg <= 0 ||
|
|
37
|
+
objectSizeArg > maximumCopyObjectSizeBytes
|
|
38
|
+
) {
|
|
39
|
+
throw new RangeError(
|
|
40
|
+
`A multipart copy needs a source of 1 byte to ${maximumCopyObjectSizeBytes} bytes, not ${objectSizeArg}.`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
const partSize = Math.max(
|
|
44
|
+
partSizeArg,
|
|
45
|
+
Math.ceil(objectSizeArg / maximumCopyPartCount),
|
|
46
|
+
);
|
|
47
|
+
if (partSize > maximumCopyPartSizeBytes) {
|
|
48
|
+
throw new RangeError(
|
|
49
|
+
`A multipart copy part cannot exceed ${maximumCopyPartSizeBytes} bytes.`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
partSize,
|
|
54
|
+
partCount: Math.ceil(objectSizeArg / partSize),
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export interface IMultipartCopyOptions {
|
|
59
|
+
copySource: string;
|
|
60
|
+
sourceSize: number;
|
|
61
|
+
/** Every part copy requires the source to still carry this ETag. */
|
|
62
|
+
sourceEtag: string;
|
|
63
|
+
targetBucketName: string;
|
|
64
|
+
targetKey: string;
|
|
65
|
+
/** Object properties of the new object; a multipart upload copies none itself. */
|
|
66
|
+
objectProperties: Pick<
|
|
67
|
+
plugins.s3.CreateMultipartUploadCommandInput,
|
|
68
|
+
| 'Metadata'
|
|
69
|
+
| 'ContentType'
|
|
70
|
+
| 'CacheControl'
|
|
71
|
+
| 'ContentDisposition'
|
|
72
|
+
| 'ContentEncoding'
|
|
73
|
+
| 'ContentLanguage'
|
|
74
|
+
| 'Expires'
|
|
75
|
+
>;
|
|
76
|
+
partSize?: number;
|
|
77
|
+
/** Receives cleanup that outlived its deadline; the operation must not finish before it settles. */
|
|
78
|
+
onDeferredCleanup: (settlementArg: Promise<void>) => void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Copies one object part by part. Any failure or abort aborts the multipart
|
|
83
|
+
* upload instead of completing it.
|
|
84
|
+
*/
|
|
85
|
+
export const copyObjectMultipart = async (
|
|
86
|
+
operationArg: IBasicBucketOperation,
|
|
87
|
+
optionsArg: IMultipartCopyOptions,
|
|
88
|
+
): Promise<void> => {
|
|
89
|
+
const plan = planMultipartCopy(optionsArg.sourceSize, optionsArg.partSize);
|
|
90
|
+
const abortUpload = (uploadIdArg: string) =>
|
|
91
|
+
abortMultipartUpload(
|
|
92
|
+
operationArg,
|
|
93
|
+
optionsArg.targetKey,
|
|
94
|
+
uploadIdArg,
|
|
95
|
+
optionsArg.targetBucketName,
|
|
96
|
+
);
|
|
97
|
+
const createOutput = await operationArg.runProvider(
|
|
98
|
+
() => operationArg.client.send(
|
|
99
|
+
new plugins.s3.CreateMultipartUploadCommand({
|
|
100
|
+
Bucket: optionsArg.targetBucketName,
|
|
101
|
+
Key: optionsArg.targetKey,
|
|
102
|
+
...optionsArg.objectProperties,
|
|
103
|
+
}),
|
|
104
|
+
{ abortSignal: operationArg.signal },
|
|
105
|
+
),
|
|
106
|
+
{
|
|
107
|
+
// A create that ignored cancellation still owns an upload.
|
|
108
|
+
disposeLateResult: async (lateOutputArg) => {
|
|
109
|
+
if (lateOutputArg.UploadId) {
|
|
110
|
+
const abortResult = await abortUpload(lateOutputArg.UploadId);
|
|
111
|
+
await abortResult.settlement;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
const uploadId = createOutput.UploadId;
|
|
117
|
+
if (typeof uploadId !== 'string' || uploadId.length === 0) {
|
|
118
|
+
throw new Error('CreateMultipartUpload returned no UploadId.');
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const parts: plugins.s3.CompletedPart[] = [];
|
|
122
|
+
for (let partNumber = 1; partNumber <= plan.partCount; partNumber++) {
|
|
123
|
+
const start = (partNumber - 1) * plan.partSize;
|
|
124
|
+
const end = Math.min(optionsArg.sourceSize, start + plan.partSize) - 1;
|
|
125
|
+
const partOutput = await operationArg.runProvider(
|
|
126
|
+
() => operationArg.client.send(
|
|
127
|
+
new plugins.s3.UploadPartCopyCommand({
|
|
128
|
+
Bucket: optionsArg.targetBucketName,
|
|
129
|
+
Key: optionsArg.targetKey,
|
|
130
|
+
UploadId: uploadId,
|
|
131
|
+
PartNumber: partNumber,
|
|
132
|
+
CopySource: optionsArg.copySource,
|
|
133
|
+
CopySourceRange: `bytes=${start}-${end}`,
|
|
134
|
+
CopySourceIfMatch: optionsArg.sourceEtag,
|
|
135
|
+
}),
|
|
136
|
+
{ abortSignal: operationArg.signal },
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
const etag = partOutput.CopyPartResult?.ETag;
|
|
140
|
+
if (typeof etag !== 'string' || etag.length === 0) {
|
|
141
|
+
throw new Error('UploadPartCopy returned no ETag.');
|
|
142
|
+
}
|
|
143
|
+
parts.push({ PartNumber: partNumber, ETag: etag });
|
|
144
|
+
}
|
|
145
|
+
await operationArg.runProvider(
|
|
146
|
+
() => operationArg.client.send(
|
|
147
|
+
new plugins.s3.CompleteMultipartUploadCommand({
|
|
148
|
+
Bucket: optionsArg.targetBucketName,
|
|
149
|
+
Key: optionsArg.targetKey,
|
|
150
|
+
UploadId: uploadId,
|
|
151
|
+
MultipartUpload: { Parts: parts },
|
|
152
|
+
}),
|
|
153
|
+
{ abortSignal: operationArg.signal },
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
const abortResult = await abortUpload(uploadId);
|
|
158
|
+
if (abortResult.cleanup.status === 'abort-failed') {
|
|
159
|
+
throw new AggregateError(
|
|
160
|
+
[error, abortResult.cleanup.cause],
|
|
161
|
+
`The multipart copy failed and its upload '${uploadId}' could not be aborted.`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (abortResult.timedOut) {
|
|
165
|
+
optionsArg.onDeferredCleanup(abortResult.settlement);
|
|
166
|
+
}
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
// node native
|
|
4
4
|
import * as crypto from 'node:crypto';
|
|
5
|
+
import * as fs from 'node:fs';
|
|
5
6
|
import * as path from 'node:path';
|
|
6
7
|
import * as stream from 'node:stream';
|
|
7
8
|
import * as nodeUtil from 'node:util';
|
|
8
9
|
|
|
9
|
-
export { crypto, nodeUtil, path, stream };
|
|
10
|
+
export { crypto, fs, nodeUtil, path, stream };
|
|
10
11
|
|
|
11
12
|
// @push.rocks scope
|
|
12
13
|
import * as smartmime from '@push.rocks/smartmime';
|