@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.
Files changed (33) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/nosqldb/classes.doc.d.ts +5 -5
  3. package/dist_ts/nosqldb/classes.doc.js +1 -1
  4. package/dist_ts/objectstorage/classes.bucket.d.ts +14 -1
  5. package/dist_ts/objectstorage/classes.bucket.js +74 -4
  6. package/dist_ts/objectstorage/classes.objectstreamlengtherror.d.ts +12 -0
  7. package/dist_ts/objectstorage/classes.objectstreamlengtherror.js +16 -0
  8. package/dist_ts/objectstorage/classes.smartbucket.js +5 -5
  9. package/dist_ts/objectstorage/index.d.ts +1 -0
  10. package/dist_ts/objectstorage/index.js +2 -1
  11. package/dist_ts/objectstorage/interfaces.d.ts +5 -1
  12. package/dist_ts/objectstorage/internal.bucketrequests.d.ts +6 -0
  13. package/dist_ts/objectstorage/internal.bucketrequests.js +12 -3
  14. package/dist_ts/objectstorage/internal.exactupload.operations.d.ts +8 -1
  15. package/dist_ts/objectstorage/internal.exactupload.operations.js +3 -3
  16. package/dist_ts/objectstorage/internal.multipartcopy.d.ts +38 -0
  17. package/dist_ts/objectstorage/internal.multipartcopy.js +98 -0
  18. package/dist_ts/objectstorage/plugins.d.ts +2 -1
  19. package/dist_ts/objectstorage/plugins.js +3 -2
  20. package/licenses/SOURCES.md +2 -2
  21. package/package.json +9 -12
  22. package/readme.md +10 -0
  23. package/ts/00_commitinfo_data.ts +1 -1
  24. package/ts/nosqldb/classes.doc.ts +5 -5
  25. package/ts/objectstorage/classes.bucket.ts +78 -3
  26. package/ts/objectstorage/classes.objectstreamlengtherror.ts +15 -0
  27. package/ts/objectstorage/classes.smartbucket.ts +5 -5
  28. package/ts/objectstorage/index.ts +1 -0
  29. package/ts/objectstorage/interfaces.ts +5 -0
  30. package/ts/objectstorage/internal.bucketrequests.ts +12 -2
  31. package/ts/objectstorage/internal.exactupload.operations.ts +4 -3
  32. package/ts/objectstorage/internal.multipartcopy.ts +169 -0
  33. package/ts/objectstorage/plugins.ts +2 -1
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@lossless.org/client',
6
- version: '2.2.0',
6
+ version: '2.4.0',
7
7
  description: 'One typed client for NoSQLDB, MongoDB, SQLDB, MariaDB, ClickHouse and S3 object storage.'
8
8
  }
@@ -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 with fallback to static for Deno compatibility
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 to avoid shadowing in Deno
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 to avoid shadowing in Deno
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 to avoid shadowing in Deno
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 to avoid shadowing in Deno
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
- * store file as stream
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: optionsArg.destinationPath || optionsArg.sourcePath,
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
- operation.finish();
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('unavailable', startedAt);
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
- const { name, httpStatusCode } = getProviderStatus(error);
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
- operationArg.bucketName,
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';