@aws-sdk/lib-transfer-manager 0.0.0-stage → 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.
Files changed (60) hide show
  1. package/README.md +303 -2
  2. package/dist-cjs/index.js +2 -0
  3. package/dist-cjs/submodules/transfer-manager/index.js +3166 -0
  4. package/dist-cjs/submodules/worker/index.js +579 -0
  5. package/dist-es/index.js +1 -0
  6. package/dist-es/submodules/transfer-manager/S3TransferManager.js +2229 -0
  7. package/dist-es/submodules/transfer-manager/chunker.js +115 -0
  8. package/dist-es/submodules/transfer-manager/directory-transfer-utils.js +102 -0
  9. package/dist-es/submodules/transfer-manager/event-listener-types.js +1 -0
  10. package/dist-es/submodules/transfer-manager/file-manager.js +70 -0
  11. package/dist-es/submodules/transfer-manager/index.browser.js +11 -0
  12. package/dist-es/submodules/transfer-manager/index.js +11 -0
  13. package/dist-es/submodules/transfer-manager/join-streams.browser.js +101 -0
  14. package/dist-es/submodules/transfer-manager/join-streams.js +105 -0
  15. package/dist-es/submodules/transfer-manager/log-level.js +30 -0
  16. package/dist-es/submodules/transfer-manager/ordered-part-queue.js +57 -0
  17. package/dist-es/submodules/transfer-manager/types.js +1 -0
  18. package/dist-es/submodules/transfer-manager/worker-http-handler.browser.js +25 -0
  19. package/dist-es/submodules/transfer-manager/worker-http-handler.js +453 -0
  20. package/dist-es/submodules/worker/http-request-worker.js +578 -0
  21. package/dist-es/submodules/worker/index.js +1 -0
  22. package/dist-types/index.d.ts +2 -0
  23. package/dist-types/submodules/transfer-manager/S3TransferManager.d.ts +60 -0
  24. package/dist-types/submodules/transfer-manager/chunker.d.ts +7 -0
  25. package/dist-types/submodules/transfer-manager/directory-transfer-utils.d.ts +90 -0
  26. package/dist-types/submodules/transfer-manager/event-listener-types.d.ts +54 -0
  27. package/dist-types/submodules/transfer-manager/file-manager.d.ts +51 -0
  28. package/dist-types/submodules/transfer-manager/index.browser.d.ts +10 -0
  29. package/dist-types/submodules/transfer-manager/index.d.ts +10 -0
  30. package/dist-types/submodules/transfer-manager/join-streams.browser.d.ts +21 -0
  31. package/dist-types/submodules/transfer-manager/join-streams.d.ts +26 -0
  32. package/dist-types/submodules/transfer-manager/log-level.d.ts +13 -0
  33. package/dist-types/submodules/transfer-manager/ordered-part-queue.d.ts +73 -0
  34. package/dist-types/submodules/transfer-manager/types.d.ts +473 -0
  35. package/dist-types/submodules/transfer-manager/worker-http-handler.browser.d.ts +20 -0
  36. package/dist-types/submodules/transfer-manager/worker-http-handler.d.ts +410 -0
  37. package/dist-types/submodules/worker/http-request-worker.d.ts +1 -0
  38. package/dist-types/submodules/worker/index.d.ts +1 -0
  39. package/dist-types/ts3.4/index.d.ts +2 -0
  40. package/dist-types/ts3.4/submodules/transfer-manager/S3TransferManager.d.ts +38 -0
  41. package/dist-types/ts3.4/submodules/transfer-manager/chunker.d.ts +10 -0
  42. package/dist-types/ts3.4/submodules/transfer-manager/directory-transfer-utils.d.ts +23 -0
  43. package/dist-types/ts3.4/submodules/transfer-manager/event-listener-types.d.ts +12 -0
  44. package/dist-types/ts3.4/submodules/transfer-manager/file-manager.d.ts +9 -0
  45. package/dist-types/ts3.4/submodules/transfer-manager/index.browser.d.ts +3 -0
  46. package/dist-types/ts3.4/submodules/transfer-manager/index.d.ts +3 -0
  47. package/dist-types/ts3.4/submodules/transfer-manager/join-streams.browser.d.ts +13 -0
  48. package/dist-types/ts3.4/submodules/transfer-manager/join-streams.d.ts +13 -0
  49. package/dist-types/ts3.4/submodules/transfer-manager/log-level.d.ts +13 -0
  50. package/dist-types/ts3.4/submodules/transfer-manager/ordered-part-queue.d.ts +18 -0
  51. package/dist-types/ts3.4/submodules/transfer-manager/types.d.ts +184 -0
  52. package/dist-types/ts3.4/submodules/transfer-manager/worker-http-handler.browser.d.ts +17 -0
  53. package/dist-types/ts3.4/submodules/transfer-manager/worker-http-handler.d.ts +241 -0
  54. package/dist-types/ts3.4/submodules/worker/http-request-worker.d.ts +1 -0
  55. package/dist-types/ts3.4/submodules/worker/index.d.ts +1 -0
  56. package/package.json +108 -4
  57. package/transfer-manager.d.ts +5 -0
  58. package/transfer-manager.js +5 -0
  59. package/worker.d.ts +5 -0
  60. package/worker.js +5 -0
@@ -0,0 +1,3166 @@
1
+ const { isReadableStream, sdkStreamMixin } = require("@smithy/core/serde");
2
+ const { Readable } = require("node:stream");
3
+ const { S3Client, GetObjectCommand, ListObjectsV2Command, HeadObjectCommand, PutObjectCommand, CreateMultipartUploadCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, UploadPartCommand } = require("@aws-sdk/client-s3");
4
+ const { isThrottlingError, isTransientError } = require("@smithy/core/retry");
5
+ const { ReadStream, lstatSync, existsSync, unlinkSync, createReadStream } = require("node:fs");
6
+ const { stat, mkdir, realpath, open, rename, unlink, opendir } = require("node:fs/promises");
7
+ const path = require("node:path");
8
+ const { resolve, sep, isAbsolute, relative, dirname, join } = require("node:path");
9
+ const { Buffer: Buffer$1 } = require("buffer");
10
+ const { randomBytes } = require("node:crypto");
11
+ const { HttpResponse } = require("@smithy/core/protocols");
12
+ const { NodeHttpHandler } = require("@smithy/node-http-handler");
13
+ const { cpus } = require("node:os");
14
+ const { Worker } = require("node:worker_threads");
15
+
16
+ async function joinStreams(streams, eventListeners) {
17
+ const firstStream = await streams[0];
18
+ if (isReadableStream(firstStream)) {
19
+ const newReadableStream = new ReadableStream({
20
+ async start(controller) {
21
+ for await (const chunk of iterateStreams(streams, eventListeners)) {
22
+ controller.enqueue(chunk);
23
+ }
24
+ controller.close();
25
+ },
26
+ });
27
+ return sdkStreamMixin(newReadableStream);
28
+ }
29
+ else {
30
+ streams.forEach((stream) => stream?.catch(() => { }));
31
+ return sdkStreamMixin(Readable.from(iterateStreams(streams, eventListeners)));
32
+ }
33
+ }
34
+ async function* iterateStreams(promises, eventListeners) {
35
+ for (const p of promises) {
36
+ p?.catch(() => { });
37
+ }
38
+ let bytesTransferred = 0;
39
+ let index = 0;
40
+ for (const streamPromise of promises) {
41
+ if (!streamPromise) {
42
+ throw new Error("Stream promise is undefined - lazy prefetch may not have completed");
43
+ }
44
+ let stream;
45
+ try {
46
+ stream = await streamPromise;
47
+ }
48
+ catch (e) {
49
+ await destroyStreams(promises);
50
+ eventListeners?.onFailure?.(e, index);
51
+ throw e;
52
+ }
53
+ if (isReadableStream(stream)) {
54
+ const reader = stream.getReader();
55
+ try {
56
+ while (true) {
57
+ const { done, value } = await reader.read();
58
+ if (done) {
59
+ break;
60
+ }
61
+ yield value;
62
+ bytesTransferred += value.byteLength;
63
+ eventListeners?.onBytes?.(bytesTransferred, index);
64
+ }
65
+ }
66
+ catch (e) {
67
+ await destroyStreams(promises);
68
+ eventListeners?.onFailure?.(e, index);
69
+ throw e;
70
+ }
71
+ finally {
72
+ reader.releaseLock();
73
+ }
74
+ }
75
+ else if (stream instanceof Readable) {
76
+ try {
77
+ for await (const chunk of stream) {
78
+ yield chunk;
79
+ const chunkSize = Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
80
+ bytesTransferred += chunkSize;
81
+ eventListeners?.onBytes?.(bytesTransferred, index);
82
+ }
83
+ }
84
+ catch (e) {
85
+ await destroyStreams(promises);
86
+ eventListeners?.onFailure?.(e, index);
87
+ throw e;
88
+ }
89
+ }
90
+ else {
91
+ await destroyStreams(promises);
92
+ const failure = new Error(`unhandled stream type ${stream?.constructor?.name}`);
93
+ eventListeners?.onFailure?.(failure, index);
94
+ throw failure;
95
+ }
96
+ eventListeners?.onStreamConsumed?.(index);
97
+ index++;
98
+ }
99
+ eventListeners?.onCompletion?.(bytesTransferred, index - 1);
100
+ }
101
+ async function destroyStreams(promises) {
102
+ await Promise.all(promises.map(async (streamPromise) => {
103
+ if (!streamPromise)
104
+ return;
105
+ return streamPromise
106
+ .then((stream) => {
107
+ if (stream instanceof Readable) {
108
+ stream.on("error", () => { });
109
+ stream.destroy();
110
+ return;
111
+ }
112
+ else if (isReadableStream(stream)) {
113
+ return stream.cancel();
114
+ }
115
+ })
116
+ .catch((e) => { });
117
+ }));
118
+ }
119
+
120
+ const byteLength = (input) => {
121
+ if (input == null) {
122
+ return 0;
123
+ }
124
+ if (typeof input === "string") {
125
+ return Buffer$1.byteLength(input);
126
+ }
127
+ if (typeof input.byteLength === "number") {
128
+ return input.byteLength;
129
+ }
130
+ else if (typeof input.length === "number") {
131
+ return input.length;
132
+ }
133
+ else if (typeof input.size === "number") {
134
+ return input.size;
135
+ }
136
+ else if (input instanceof ReadStream) {
137
+ const { start: rangeStart, end: rangeEnd } = input;
138
+ let size;
139
+ try {
140
+ size = lstatSync(input.path).size;
141
+ }
142
+ catch {
143
+ return undefined;
144
+ }
145
+ const start = typeof rangeStart === "number" ? rangeStart : 0;
146
+ const endExclusive = Number.isFinite(rangeEnd) ? Math.min(rangeEnd + 1, size) : size;
147
+ return Math.max(0, endExclusive - start);
148
+ }
149
+ else if (typeof input.start === "number" && typeof input.end === "number") {
150
+ return input.end + 1 - input.start;
151
+ }
152
+ return undefined;
153
+ };
154
+ async function* getChunkStream(data, partSize, getNextData) {
155
+ let partNumber = 1;
156
+ const currentBuffer = { chunks: [], length: 0 };
157
+ for await (const datum of getNextData(data)) {
158
+ currentBuffer.chunks.push(datum);
159
+ currentBuffer.length += datum.byteLength;
160
+ while (currentBuffer.length > partSize) {
161
+ const dataChunk = currentBuffer.chunks.length > 1 ? Buffer$1.concat(currentBuffer.chunks) : currentBuffer.chunks[0];
162
+ yield { partNumber, data: dataChunk.subarray(0, partSize) };
163
+ currentBuffer.chunks = [dataChunk.subarray(partSize)];
164
+ currentBuffer.length = currentBuffer.chunks[0].byteLength;
165
+ partNumber += 1;
166
+ }
167
+ }
168
+ yield {
169
+ partNumber,
170
+ data: currentBuffer.chunks.length !== 1 ? Buffer$1.concat(currentBuffer.chunks) : currentBuffer.chunks[0],
171
+ lastPart: true,
172
+ };
173
+ }
174
+ async function* getChunkUint8Array(data, partSize) {
175
+ let partNumber = 1;
176
+ let startByte = 0;
177
+ let endByte = partSize;
178
+ while (endByte < data.byteLength) {
179
+ yield { partNumber, data: data.subarray(startByte, endByte) };
180
+ partNumber += 1;
181
+ startByte = endByte;
182
+ endByte = startByte + partSize;
183
+ }
184
+ yield { partNumber, data: data.subarray(startByte), lastPart: true };
185
+ }
186
+ async function* getDataReadable(data) {
187
+ for await (const chunk of data) {
188
+ if (Buffer$1.isBuffer(chunk) || chunk instanceof Uint8Array) {
189
+ yield chunk;
190
+ }
191
+ else {
192
+ yield Buffer$1.from(chunk);
193
+ }
194
+ }
195
+ }
196
+ async function* getDataReadableStream(data) {
197
+ const reader = data.getReader();
198
+ try {
199
+ while (true) {
200
+ const { done, value } = await reader.read();
201
+ if (done)
202
+ return;
203
+ if (Buffer$1.isBuffer(value) || value instanceof Uint8Array) {
204
+ yield value;
205
+ }
206
+ else {
207
+ yield Buffer$1.from(value);
208
+ }
209
+ }
210
+ }
211
+ finally {
212
+ reader.releaseLock();
213
+ }
214
+ }
215
+ const getChunk = (data, partSize) => {
216
+ if (data instanceof Uint8Array) {
217
+ return getChunkUint8Array(data, partSize);
218
+ }
219
+ if (data instanceof String || typeof data === "string") {
220
+ return getChunkUint8Array(Buffer$1.from(data), partSize);
221
+ }
222
+ if (data instanceof ReadableStream) {
223
+ return getChunkStream(data, partSize, getDataReadableStream);
224
+ }
225
+ if (typeof data.stream === "function") {
226
+ return getChunkStream(data.stream(), partSize, getDataReadableStream);
227
+ }
228
+ if (Symbol.asyncIterator in data || typeof data[Symbol.asyncIterator] === "function") {
229
+ return getChunkStream(data, partSize, getDataReadable);
230
+ }
231
+ throw new Error("Body Data is unsupported format, expected data to be one of: string | Uint8Array | Buffer | Readable | ReadableStream | Blob;.");
232
+ };
233
+
234
+ class Semaphore {
235
+ limit;
236
+ inFlightRequests = 0;
237
+ waiters = [];
238
+ constructor(limit) {
239
+ this.limit = limit;
240
+ }
241
+ async acquire(count = 1) {
242
+ if (this.inFlightRequests < this.limit) {
243
+ this.inFlightRequests += count;
244
+ return;
245
+ }
246
+ await new Promise((resolve) => this.waiters.push(resolve));
247
+ this.inFlightRequests += count;
248
+ }
249
+ release(count = 1) {
250
+ this.inFlightRequests -= count;
251
+ while (this.waiters.length > 0 && this.inFlightRequests < this.limit) {
252
+ const next = this.waiters.shift();
253
+ if (next) {
254
+ next();
255
+ }
256
+ }
257
+ }
258
+ }
259
+ async function validateDirectory(dirPath) {
260
+ const absolutePath = resolve(dirPath);
261
+ let stats;
262
+ try {
263
+ stats = await stat(absolutePath);
264
+ }
265
+ catch (error) {
266
+ throw new Error(`Cannot access directory at: ${absolutePath}, ${error.message}`);
267
+ }
268
+ if (!stats.isDirectory()) {
269
+ throw new Error(`Path is not a directory: ${absolutePath}`);
270
+ }
271
+ return absolutePath;
272
+ }
273
+ async function handleFailure(policy, context, abortController) {
274
+ if (policy === "terminate") {
275
+ abortController.abort();
276
+ return "terminate";
277
+ }
278
+ if (policy === "continue") {
279
+ return "continue";
280
+ }
281
+ try {
282
+ const result = await policy(context);
283
+ if (result === "terminate") {
284
+ abortController.abort();
285
+ }
286
+ return result;
287
+ }
288
+ catch {
289
+ abortController.abort();
290
+ return "terminate";
291
+ }
292
+ }
293
+ async function createDestinationDirectory(dirPath) {
294
+ const absolutePath = resolve(dirPath);
295
+ if (existsSync(absolutePath)) {
296
+ return validateDirectory(absolutePath);
297
+ }
298
+ await mkdir(absolutePath, { recursive: true });
299
+ return absolutePath;
300
+ }
301
+ function isFolderObject(object) {
302
+ return (object.Key?.endsWith("/") ?? false) && (object.Size ?? 0) === 0;
303
+ }
304
+ function deriveLocalPath(destination, key) {
305
+ const normalizedKey = sep === "/" ? key : key.split("/").join(sep);
306
+ if (isAbsolute(normalizedKey)) {
307
+ throw new Error(`Object key resolves to an absolute path and would escape the destination directory: ${key}`);
308
+ }
309
+ const fullPath = resolve(destination, normalizedKey);
310
+ const destinationWithSep = destination.endsWith(sep) ? destination : destination + sep;
311
+ if (fullPath !== destination && !fullPath.startsWith(destinationWithSep)) {
312
+ throw new Error(`Object key "${key}" resolves to a path outside the destination directory. Path traversal is not allowed.`);
313
+ }
314
+ return fullPath;
315
+ }
316
+ async function createLocalParentDirectories(destination, localPath) {
317
+ const canonicalDestination = await realpath(destination);
318
+ const relativeParent = relative(destination, dirname(localPath));
319
+ let currentDirectory = destination;
320
+ for (const segment of relativeParent.split(sep)) {
321
+ if (!segment || segment === ".") {
322
+ continue;
323
+ }
324
+ currentDirectory = join(currentDirectory, segment);
325
+ await mkdir(currentDirectory, { recursive: true });
326
+ const canonicalDirectory = await realpath(currentDirectory);
327
+ const canonicalRelative = relative(canonicalDestination, canonicalDirectory);
328
+ if (canonicalRelative === ".." || canonicalRelative.startsWith(`..${sep}`) || isAbsolute(canonicalRelative)) {
329
+ throw new Error("Symbolic links outside the destination are not allowed.");
330
+ }
331
+ }
332
+ }
333
+
334
+ class FileManager {
335
+ cleanupHandlers = new Map();
336
+ async createTempFile(destination, totalSize) {
337
+ const tempFilePath = `${destination}.s3tmp.${this.generateUniqueId()}`;
338
+ const fd = await open(tempFilePath, "wx");
339
+ try {
340
+ await fd.truncate(totalSize);
341
+ }
342
+ finally {
343
+ await fd.close();
344
+ }
345
+ return tempFilePath;
346
+ }
347
+ async atomicRename(tempFilePath, destination) {
348
+ try {
349
+ await rename(tempFilePath, destination);
350
+ }
351
+ catch (renameError) {
352
+ if (renameError.code === "EEXIST" || renameError.code === "EPERM") {
353
+ try {
354
+ await unlink(destination);
355
+ await rename(tempFilePath, destination);
356
+ }
357
+ catch (fallbackError) {
358
+ await this.deleteTempFile(tempFilePath);
359
+ throw new Error(`Failed to rename temp file to destination: ${fallbackError.message || fallbackError.code}`);
360
+ }
361
+ }
362
+ else {
363
+ await this.deleteTempFile(tempFilePath);
364
+ throw new Error(`Failed to rename temp file to destination: ${renameError.message || renameError.code}`);
365
+ }
366
+ }
367
+ }
368
+ async deleteTempFile(tempFilePath) {
369
+ this.unregisterCleanupHandler(tempFilePath);
370
+ try {
371
+ await unlink(tempFilePath);
372
+ }
373
+ catch (error) {
374
+ if (error.code !== "ENOENT") {
375
+ throw error;
376
+ }
377
+ }
378
+ }
379
+ registerCleanupHandler(tempFilePath) {
380
+ const exitHandler = () => {
381
+ try {
382
+ unlinkSync(tempFilePath);
383
+ }
384
+ catch {
385
+ }
386
+ };
387
+ process.on("exit", exitHandler);
388
+ this.cleanupHandlers.set(tempFilePath, { exitHandler });
389
+ }
390
+ unregisterCleanupHandler(tempFilePath) {
391
+ const handlers = this.cleanupHandlers.get(tempFilePath);
392
+ if (handlers) {
393
+ process.removeListener("exit", handlers.exitHandler);
394
+ this.cleanupHandlers.delete(tempFilePath);
395
+ }
396
+ }
397
+ generateUniqueId() {
398
+ return randomBytes(4).toString("hex");
399
+ }
400
+ }
401
+
402
+ class LogLevel {
403
+ minimum;
404
+ logger;
405
+ static LEVELS = ["trace", "debug", "info", "warn", "error"];
406
+ level = 5;
407
+ constructor(minimum, logger = console) {
408
+ this.minimum = minimum;
409
+ this.logger = logger;
410
+ const index = LogLevel.LEVELS.indexOf(minimum);
411
+ if (index === -1) {
412
+ throw new Error(`Log level must be one of ${LogLevel.LEVELS.join(", ")}`);
413
+ }
414
+ this.level = index;
415
+ }
416
+ trace(...args) {
417
+ this.level <= 0 && this.logger.trace?.(...args);
418
+ }
419
+ debug(...args) {
420
+ this.level <= 1 && this.logger.debug?.(...args);
421
+ }
422
+ info(...args) {
423
+ this.level <= 2 && this.logger.info?.(...args);
424
+ }
425
+ warn(...args) {
426
+ this.level <= 3 && this.logger.warn?.(...args);
427
+ }
428
+ error(...args) {
429
+ this.level <= 4 && this.logger.error?.(...args);
430
+ }
431
+ }
432
+
433
+ class OrderedPartQueue {
434
+ pending = new Map();
435
+ nextExpected = 0;
436
+ totalParts;
437
+ waitingConsumer = null;
438
+ error = undefined;
439
+ constructor(totalParts) {
440
+ this.totalParts = totalParts;
441
+ }
442
+ enqueue(rangeIndex, buffer, byteLength) {
443
+ if (this.error)
444
+ return;
445
+ if (rangeIndex === this.nextExpected && this.waitingConsumer) {
446
+ const consumer = this.waitingConsumer;
447
+ this.waitingConsumer = null;
448
+ this.nextExpected++;
449
+ consumer({ buffer, byteLength });
450
+ }
451
+ else {
452
+ this.pending.set(rangeIndex, { buffer, byteLength });
453
+ }
454
+ }
455
+ async dequeue() {
456
+ if (this.error) {
457
+ throw this.error;
458
+ }
459
+ if (this.nextExpected >= this.totalParts) {
460
+ return null;
461
+ }
462
+ const ready = this.pending.get(this.nextExpected);
463
+ if (ready) {
464
+ this.pending.delete(this.nextExpected);
465
+ this.nextExpected++;
466
+ return ready;
467
+ }
468
+ return new Promise((resolve) => {
469
+ this.waitingConsumer = resolve;
470
+ });
471
+ }
472
+ setError(error) {
473
+ this.error = error;
474
+ if (this.waitingConsumer) {
475
+ const consumer = this.waitingConsumer;
476
+ this.waitingConsumer = null;
477
+ consumer(null);
478
+ }
479
+ }
480
+ hasError() {
481
+ return this.error !== undefined;
482
+ }
483
+ getError() {
484
+ return this.error;
485
+ }
486
+ get pendingCount() {
487
+ return this.pending.size;
488
+ }
489
+ }
490
+
491
+ class S3TransferManagerBase {
492
+ static MIN_PART_SIZE = 5 * 1024 * 1024;
493
+ s3;
494
+ targetPartSizeBytes;
495
+ multipartUploadThresholdBytes;
496
+ requestChecksumCalculation;
497
+ responseChecksumValidation;
498
+ multipartDownloadType;
499
+ eventListeners;
500
+ abortCleanupFunctions = new WeakMap();
501
+ maxConcurrentDownloads;
502
+ maxConcurrentUploads;
503
+ workerThreadCount;
504
+ workerHttpHandler;
505
+ logger;
506
+ deps;
507
+ constructor(config, deps) {
508
+ this.deps = deps;
509
+ this.requestChecksumCalculation = config.requestChecksumCalculation ?? "WHEN_SUPPORTED";
510
+ this.responseChecksumValidation = config.responseChecksumValidation ?? "WHEN_SUPPORTED";
511
+ this.maxConcurrentUploads = config.maxConcurrentUploads ?? 32;
512
+ this.maxConcurrentDownloads = config.maxConcurrentDownloads ?? 32;
513
+ this.workerThreadCount = config.workerThreadCount ?? this.deps.defaultWorkerCount();
514
+ this.logger = config.logger ?? new LogLevel("warn");
515
+ this.s3 =
516
+ config.s3 ??
517
+ new S3Client({
518
+ requestChecksumCalculation: this.requestChecksumCalculation,
519
+ responseChecksumValidation: this.responseChecksumValidation,
520
+ });
521
+ if (this.workerThreadCount > 1) {
522
+ this.workerHttpHandler = new this.deps.WorkerHttpHandler({
523
+ workerThreadCount: this.workerThreadCount,
524
+ maxConcurrentUploads: this.maxConcurrentUploads,
525
+ maxConcurrentDownloads: this.maxConcurrentDownloads,
526
+ useODirect: config.useODirect,
527
+ logger: this.logger,
528
+ });
529
+ if (this.s3.config) {
530
+ this.s3.config.requestHandler = this.workerHttpHandler;
531
+ }
532
+ }
533
+ this.targetPartSizeBytes = config.targetPartSizeBytes ?? 8 * 1024 * 1024;
534
+ this.multipartUploadThresholdBytes = config.multipartUploadThresholdBytes ?? 16 * 1024 * 1024;
535
+ this.multipartDownloadType = config.multipartDownloadType ?? "PART";
536
+ this.eventListeners = {
537
+ transferInitiated: config.eventListeners?.transferInitiated ?? [],
538
+ bytesTransferred: config.eventListeners?.bytesTransferred ?? [],
539
+ transferComplete: config.eventListeners?.transferComplete ?? [],
540
+ transferFailed: config.eventListeners?.transferFailed ?? [],
541
+ };
542
+ this.validateConfig();
543
+ }
544
+ addEventListener(type, callback, options) {
545
+ const eventType = type;
546
+ const listeners = this.eventListeners[eventType];
547
+ if (!listeners) {
548
+ throw new Error(`Unknown event type: ${eventType}`);
549
+ }
550
+ const once = typeof options !== "boolean" && options?.once;
551
+ const signal = typeof options !== "boolean" ? options?.signal : undefined;
552
+ let updatedCallback = callback;
553
+ if (signal?.aborted) {
554
+ return;
555
+ }
556
+ if (signal) {
557
+ const removeListenerAfterAbort = () => {
558
+ this.removeEventListener(eventType, updatedCallback);
559
+ this.abortCleanupFunctions.delete(signal);
560
+ };
561
+ this.abortCleanupFunctions.set(signal, () => signal.removeEventListener("abort", removeListenerAfterAbort));
562
+ signal.addEventListener("abort", removeListenerAfterAbort, {
563
+ once: true,
564
+ });
565
+ }
566
+ if (once) {
567
+ updatedCallback = (event) => {
568
+ if (typeof callback === "function") {
569
+ callback(event);
570
+ }
571
+ else {
572
+ callback.handleEvent(event);
573
+ }
574
+ this.removeEventListener(eventType, updatedCallback);
575
+ };
576
+ }
577
+ listeners.push(updatedCallback);
578
+ }
579
+ dispatchEvent(event) {
580
+ const eventType = event.type;
581
+ const listeners = this.eventListeners[eventType];
582
+ if (listeners) {
583
+ for (const listener of listeners) {
584
+ if (typeof listener === "function") {
585
+ listener(event);
586
+ }
587
+ else {
588
+ listener.handleEvent(event);
589
+ }
590
+ }
591
+ }
592
+ return true;
593
+ }
594
+ removeEventListener(type, callback, options) {
595
+ const eventType = type;
596
+ const listeners = this.eventListeners[eventType];
597
+ if (listeners) {
598
+ if (eventType === "transferInitiated" ||
599
+ eventType === "bytesTransferred" ||
600
+ eventType === "transferFailed" ||
601
+ eventType === "transferComplete") {
602
+ const eventListener = callback;
603
+ let index = listeners.indexOf(eventListener);
604
+ while (index !== -1) {
605
+ listeners.splice(index, 1);
606
+ index = listeners.indexOf(eventListener);
607
+ }
608
+ }
609
+ else {
610
+ throw new Error(`Unknown event type: ${type}`);
611
+ }
612
+ }
613
+ }
614
+ async upload(request, transferOptions) {
615
+ return this.uploadInternal(request, transferOptions, true);
616
+ }
617
+ async uploadInternal(request, transferOptions, emitEvents = true) {
618
+ const totalContentLength = request.ContentLength ?? byteLength(request.Body);
619
+ if (totalContentLength === undefined) {
620
+ throw new Error("Unable to determine content length for upload");
621
+ }
622
+ this.checkAborted(transferOptions);
623
+ if (emitEvents) {
624
+ this.addEventListeners(transferOptions?.eventListeners);
625
+ this.dispatchTransferInitiatedEvent(request, totalContentLength);
626
+ }
627
+ const removeLocalEventListeners = () => {
628
+ this.removeEventListeners(transferOptions?.eventListeners);
629
+ if (transferOptions?.abortSignal) {
630
+ this.abortCleanupFunctions.get(transferOptions.abortSignal)?.();
631
+ this.abortCleanupFunctions.delete(transferOptions.abortSignal);
632
+ }
633
+ };
634
+ try {
635
+ let response;
636
+ if (totalContentLength < this.multipartUploadThresholdBytes) {
637
+ response = await this.uploadSinglePart(request, totalContentLength, transferOptions, emitEvents);
638
+ }
639
+ else if (this.workerThreadCount > 1 && this.isFileBody(request.Body)) {
640
+ response = await this.threadedUploadInPartsFromFile(request, totalContentLength, transferOptions, emitEvents);
641
+ }
642
+ else if (this.workerThreadCount > 1 && this.isInMemoryBody(request.Body)) {
643
+ if (typeof request.Body === "string") {
644
+ request = {
645
+ ...request,
646
+ Body: new TextEncoder().encode(request.Body),
647
+ };
648
+ }
649
+ response = await this.threadedUploadInParts(request, totalContentLength, transferOptions, emitEvents);
650
+ }
651
+ else {
652
+ response = await this.uploadInParts(request, totalContentLength, transferOptions, emitEvents);
653
+ }
654
+ if (emitEvents) {
655
+ this.dispatchEvent(Object.assign(new Event("transferComplete"), {
656
+ request,
657
+ response,
658
+ snapshot: {
659
+ transferredBytes: totalContentLength,
660
+ totalBytes: totalContentLength,
661
+ },
662
+ }));
663
+ }
664
+ removeLocalEventListeners();
665
+ return response;
666
+ }
667
+ catch (error) {
668
+ if (emitEvents) {
669
+ this.dispatchTransferFailedEvent(request, totalContentLength, error);
670
+ }
671
+ removeLocalEventListeners();
672
+ throw error;
673
+ }
674
+ }
675
+ async download(request, transferOptions) {
676
+ const partNumber = request.PartNumber;
677
+ if (typeof partNumber === "number") {
678
+ throw new Error("partNumber included: S3 Transfer Manager does not support downloads for specific parts. Use GetObjectCommand instead");
679
+ }
680
+ const metadata = {};
681
+ const streams = [];
682
+ const requests = [];
683
+ let totalSize;
684
+ this.checkAborted(transferOptions);
685
+ this.addEventListeners(transferOptions?.eventListeners);
686
+ const checksumValidationEnabled = request.ChecksumMode === "ENABLED" || this.responseChecksumValidation === "WHEN_SUPPORTED";
687
+ let onStreamConsumed;
688
+ const removeLocalEventListeners = () => {
689
+ this.removeEventListeners(transferOptions?.eventListeners);
690
+ if (transferOptions?.abortSignal) {
691
+ this.abortCleanupFunctions.get(transferOptions.abortSignal)?.();
692
+ this.abortCleanupFunctions.delete(transferOptions.abortSignal);
693
+ }
694
+ };
695
+ const resolvedTransferOptions = transferOptions ?? {};
696
+ const downloadArgs = [
697
+ request,
698
+ resolvedTransferOptions,
699
+ streams,
700
+ requests,
701
+ metadata,
702
+ checksumValidationEnabled,
703
+ ];
704
+ try {
705
+ let responseMetadata;
706
+ if (this.multipartDownloadType === "PART") {
707
+ if (this.workerHttpHandler) {
708
+ responseMetadata = await this.downloadByPartWithWorkers(...downloadArgs);
709
+ }
710
+ else {
711
+ responseMetadata = await this.downloadByPart(...downloadArgs);
712
+ }
713
+ }
714
+ else if (this.multipartDownloadType === "RANGE") {
715
+ if (this.workerHttpHandler && !request.Range) {
716
+ responseMetadata = await this.downloadByRangeWithWorkers(...downloadArgs);
717
+ }
718
+ else {
719
+ responseMetadata = await this.downloadByRange(...downloadArgs);
720
+ }
721
+ }
722
+ totalSize = responseMetadata.totalSize;
723
+ onStreamConsumed = responseMetadata.onStreamConsumed;
724
+ }
725
+ catch (error) {
726
+ await this.deps.destroyStreams(streams);
727
+ removeLocalEventListeners();
728
+ throw error;
729
+ }
730
+ const response = {
731
+ ...metadata,
732
+ Body: await this.deps.joinStreams(streams, {
733
+ onBytes: (byteLength, index) => {
734
+ this.dispatchEvent(Object.assign(new Event("bytesTransferred"), {
735
+ request: requests[index],
736
+ snapshot: {
737
+ transferredBytes: byteLength,
738
+ totalBytes: totalSize,
739
+ },
740
+ }));
741
+ },
742
+ onCompletion: (byteLength, index) => {
743
+ this.dispatchEvent(Object.assign(new Event("transferComplete"), {
744
+ request: requests[index],
745
+ response,
746
+ snapshot: {
747
+ transferredBytes: byteLength,
748
+ totalBytes: totalSize,
749
+ },
750
+ }));
751
+ removeLocalEventListeners();
752
+ },
753
+ onFailure: (error, index) => {
754
+ this.dispatchEvent(Object.assign(new Event("transferFailed"), {
755
+ request: requests[index],
756
+ snapshot: {
757
+ transferredBytes: error,
758
+ totalBytes: totalSize,
759
+ },
760
+ }));
761
+ removeLocalEventListeners();
762
+ },
763
+ onStreamConsumed,
764
+ }),
765
+ };
766
+ return response;
767
+ }
768
+ async downloadToFile(request, transferOptions) {
769
+ return this.downloadToFileInternal(request, transferOptions, true);
770
+ }
771
+ async downloadToFileInternal(request, transferOptions, emitEvents = true) {
772
+ if (!request.destination || request.destination === "") {
773
+ throw new Error("destination must be a non-empty string");
774
+ }
775
+ const resolvedDestination = resolve(request.destination);
776
+ if (request.failIfExists && existsSync(resolvedDestination)) {
777
+ throw new Error(`File already exists at destination: ${resolvedDestination}`);
778
+ }
779
+ this.checkAborted(transferOptions);
780
+ if (emitEvents) {
781
+ this.addEventListeners(transferOptions?.eventListeners);
782
+ }
783
+ const removeLocalEventListeners = () => {
784
+ this.removeEventListeners(transferOptions?.eventListeners);
785
+ if (transferOptions?.abortSignal) {
786
+ this.abortCleanupFunctions.get(transferOptions.abortSignal)?.();
787
+ this.abortCleanupFunctions.delete(transferOptions.abortSignal);
788
+ }
789
+ };
790
+ const checksumValidationEnabled = request.ChecksumMode === "ENABLED" || this.responseChecksumValidation === "WHEN_SUPPORTED";
791
+ const { destination, failIfExists, ...getObjectFields } = request;
792
+ if (this.multipartDownloadType === "RANGE") {
793
+ try {
794
+ const result = await this.downloadToFileByRange(request, resolvedDestination, transferOptions, emitEvents);
795
+ removeLocalEventListeners();
796
+ return result;
797
+ }
798
+ catch (error) {
799
+ removeLocalEventListeners();
800
+ throw error;
801
+ }
802
+ }
803
+ const initialPartRequest = {
804
+ ...getObjectFields,
805
+ PartNumber: 1,
806
+ ...(checksumValidationEnabled && { ChecksumMode: "ENABLED" }),
807
+ };
808
+ let initialResponse;
809
+ try {
810
+ initialResponse = await this.s3.send(new GetObjectCommand(initialPartRequest), transferOptions);
811
+ }
812
+ catch (error) {
813
+ removeLocalEventListeners();
814
+ throw error;
815
+ }
816
+ if (initialResponse.Body && initialResponse.Body instanceof Readable) {
817
+ initialResponse.Body.on("error", () => { });
818
+ }
819
+ const totalSize = initialResponse.ContentRange
820
+ ? Number.parseInt(initialResponse.ContentRange.split("/")[1])
821
+ : (initialResponse.ContentLength ?? 0);
822
+ const partsCount = initialResponse.PartsCount ?? 1;
823
+ const eTag = initialResponse.ETag ?? undefined;
824
+ if (emitEvents) {
825
+ this.dispatchTransferInitiatedEvent(request, totalSize);
826
+ }
827
+ const fileManager = new FileManager();
828
+ let tempFilePath;
829
+ try {
830
+ if (partsCount === 1) {
831
+ tempFilePath = await fileManager.createTempFile(resolvedDestination, totalSize);
832
+ fileManager.registerCleanupHandler(tempFilePath);
833
+ await this.writeResponseBodyToFile(initialResponse.Body, tempFilePath, initialResponse.ContentRange, 0);
834
+ if (emitEvents) {
835
+ this.dispatchEvent(Object.assign(new Event("bytesTransferred"), {
836
+ request,
837
+ snapshot: {
838
+ transferredBytes: totalSize,
839
+ totalBytes: totalSize,
840
+ },
841
+ }));
842
+ }
843
+ await fileManager.atomicRename(tempFilePath, resolvedDestination);
844
+ fileManager.unregisterCleanupHandler(tempFilePath);
845
+ const response = this.buildDownloadToFileResponse(initialResponse, totalSize, request, emitEvents);
846
+ removeLocalEventListeners();
847
+ return response;
848
+ }
849
+ tempFilePath = await fileManager.createTempFile(resolvedDestination, totalSize);
850
+ fileManager.registerCleanupHandler(tempFilePath);
851
+ let transferredBytes = 0;
852
+ const partSize = initialResponse.ContentLength ?? 0;
853
+ await this.writeResponseBodyToFile(initialResponse.Body, tempFilePath, initialResponse.ContentRange, 0);
854
+ transferredBytes += partSize;
855
+ if (emitEvents) {
856
+ this.dispatchEvent(Object.assign(new Event("bytesTransferred"), {
857
+ request,
858
+ snapshot: {
859
+ transferredBytes,
860
+ totalBytes: totalSize,
861
+ },
862
+ }));
863
+ }
864
+ const partResults = [];
865
+ partResults.push({ partNumber: 1, bytesWritten: partSize });
866
+ const semaphore = new Semaphore(this.maxConcurrentDownloads);
867
+ const abortController = new AbortController();
868
+ let failed = false;
869
+ let failureError;
870
+ const userSignal = transferOptions?.abortSignal;
871
+ if (userSignal) {
872
+ const onUserAbort = () => abortController.abort();
873
+ userSignal.addEventListener("abort", onUserAbort, { once: true });
874
+ }
875
+ const remainingPartTasks = [];
876
+ for (let part = 2; part <= partsCount; part++) {
877
+ if (failed || abortController.signal.aborted)
878
+ break;
879
+ this.checkAborted(transferOptions);
880
+ await semaphore.acquire();
881
+ if (failed || abortController.signal.aborted) {
882
+ semaphore.release();
883
+ break;
884
+ }
885
+ const partNumber = part;
886
+ const partOffset = (partNumber - 1) * partSize;
887
+ const expectedLength = Math.min(partSize, totalSize - partOffset);
888
+ const partGetRequest = {
889
+ ...getObjectFields,
890
+ PartNumber: partNumber,
891
+ IfMatch: eTag,
892
+ ...(checksumValidationEnabled && {
893
+ ChecksumMode: "ENABLED",
894
+ }),
895
+ };
896
+ const task = (async () => {
897
+ try {
898
+ if (this.workerHttpHandler) {
899
+ const resultToken = `download-${partNumber}-${Date.now()}`;
900
+ const downloadOptions = {
901
+ filePath: tempFilePath,
902
+ offset: partOffset,
903
+ expectedLength,
904
+ resultToken,
905
+ };
906
+ await this.s3.send(new GetObjectCommand(partGetRequest), {
907
+ ...transferOptions,
908
+ downloadDataToFile: downloadOptions,
909
+ });
910
+ const downloadResult = this.s3.config.requestHandler.getDownloadResult(resultToken);
911
+ if (!downloadResult) {
912
+ throw new Error(`Missing download result for part ${partNumber}`);
913
+ }
914
+ partResults.push({
915
+ partNumber,
916
+ bytesWritten: downloadResult.bytesWritten,
917
+ });
918
+ }
919
+ else {
920
+ const partResponse = await this.s3.send(new GetObjectCommand(partGetRequest), transferOptions);
921
+ if (partResponse.Body && partResponse.Body instanceof Readable) {
922
+ partResponse.Body.on("error", () => { });
923
+ }
924
+ this.validatePartDownload(partResponse.ContentRange, partNumber, partSize);
925
+ await this.writeResponseBodyToFile(partResponse.Body, tempFilePath, partResponse.ContentRange, partOffset);
926
+ partResults.push({
927
+ partNumber,
928
+ bytesWritten: partResponse.ContentLength ?? expectedLength,
929
+ });
930
+ }
931
+ transferredBytes += expectedLength;
932
+ if (emitEvents) {
933
+ this.dispatchEvent(Object.assign(new Event("bytesTransferred"), {
934
+ request: partGetRequest,
935
+ snapshot: {
936
+ transferredBytes,
937
+ totalBytes: totalSize,
938
+ },
939
+ }));
940
+ }
941
+ }
942
+ catch (error) {
943
+ if (!failed) {
944
+ failed = true;
945
+ failureError = error;
946
+ abortController.abort();
947
+ }
948
+ }
949
+ finally {
950
+ semaphore.release();
951
+ }
952
+ })();
953
+ remainingPartTasks.push(task);
954
+ }
955
+ await Promise.allSettled(remainingPartTasks);
956
+ if (abortController.signal.aborted && !failed) {
957
+ const abortError = Object.assign(new Error("Transfer aborted."), {
958
+ name: "AbortError",
959
+ });
960
+ await this.handleDownloadCleanup(fileManager, tempFilePath, request, totalSize, abortError, emitEvents);
961
+ removeLocalEventListeners();
962
+ throw abortError;
963
+ }
964
+ if (failed) {
965
+ await this.handleDownloadCleanup(fileManager, tempFilePath, request, totalSize, failureError, emitEvents);
966
+ removeLocalEventListeners();
967
+ throw failureError;
968
+ }
969
+ if (partResults.length !== partsCount) {
970
+ const error = new Error(`The number of parts downloaded (${partResults.length}) does not match the expected number (${partsCount})`);
971
+ await this.handleDownloadCleanup(fileManager, tempFilePath, request, totalSize, error, emitEvents);
972
+ removeLocalEventListeners();
973
+ throw error;
974
+ }
975
+ const totalBytesWritten = partResults.reduce((sum, p) => sum + p.bytesWritten, 0);
976
+ if (totalBytesWritten !== totalSize) {
977
+ const error = new Error(`Total bytes written (${totalBytesWritten}) does not match expected object size (${totalSize})`);
978
+ await this.handleDownloadCleanup(fileManager, tempFilePath, request, totalSize, error, emitEvents);
979
+ removeLocalEventListeners();
980
+ throw error;
981
+ }
982
+ await fileManager.atomicRename(tempFilePath, resolvedDestination);
983
+ fileManager.unregisterCleanupHandler(tempFilePath);
984
+ const response = this.buildDownloadToFileResponse(initialResponse, totalSize, request, emitEvents);
985
+ removeLocalEventListeners();
986
+ return response;
987
+ }
988
+ catch (error) {
989
+ if (tempFilePath) {
990
+ await fileManager.deleteTempFile(tempFilePath).catch(() => { });
991
+ fileManager.unregisterCleanupHandler(tempFilePath);
992
+ }
993
+ removeLocalEventListeners();
994
+ throw error;
995
+ }
996
+ }
997
+ async downloadToFileByRange(request, resolvedDestination, transferOptions, emitEvents) {
998
+ const { destination, failIfExists, ...getObjectFields } = request;
999
+ const checksumValidationEnabled = request.ChecksumMode === "ENABLED" || this.responseChecksumValidation === "WHEN_SUPPORTED";
1000
+ const fileManager = new FileManager();
1001
+ let tempFilePath;
1002
+ try {
1003
+ const rangeSize = this.targetPartSizeBytes;
1004
+ const initialRangeRequest = {
1005
+ ...getObjectFields,
1006
+ Range: `bytes=0-${rangeSize - 1}`,
1007
+ ...(checksumValidationEnabled && { ChecksumMode: "ENABLED" }),
1008
+ };
1009
+ const initialResponse = await this.s3.send(new GetObjectCommand(initialRangeRequest), transferOptions);
1010
+ if (initialResponse.Body && initialResponse.Body instanceof Readable) {
1011
+ initialResponse.Body.on("error", () => { });
1012
+ }
1013
+ this.validateRangeDownload(`bytes=0-${rangeSize - 1}`, initialResponse.ContentRange);
1014
+ const totalContentLength = Number.parseInt(initialResponse.ContentRange.split("/")[1]);
1015
+ const responseContentLength = initialResponse.ContentLength ?? 0;
1016
+ if (responseContentLength === totalContentLength) {
1017
+ tempFilePath = await fileManager.createTempFile(resolvedDestination, totalContentLength);
1018
+ fileManager.registerCleanupHandler(tempFilePath);
1019
+ await this.writeResponseBodyToFile(initialResponse.Body, tempFilePath, initialResponse.ContentRange, 0);
1020
+ await fileManager.atomicRename(tempFilePath, resolvedDestination);
1021
+ fileManager.unregisterCleanupHandler(tempFilePath);
1022
+ if (emitEvents) {
1023
+ this.dispatchTransferInitiatedEvent(request, totalContentLength);
1024
+ this.dispatchEvent(Object.assign(new Event("bytesTransferred"), {
1025
+ request,
1026
+ snapshot: {
1027
+ transferredBytes: totalContentLength,
1028
+ totalBytes: totalContentLength,
1029
+ },
1030
+ }));
1031
+ }
1032
+ return this.buildDownloadToFileResponse(initialResponse, totalContentLength, request, emitEvents);
1033
+ }
1034
+ const eTag = initialResponse.ETag ?? undefined;
1035
+ const expectedRequestCount = Math.ceil(totalContentLength / rangeSize);
1036
+ if (emitEvents) {
1037
+ this.dispatchTransferInitiatedEvent(request, totalContentLength);
1038
+ }
1039
+ tempFilePath = await fileManager.createTempFile(resolvedDestination, totalContentLength);
1040
+ fileManager.registerCleanupHandler(tempFilePath);
1041
+ await this.writeResponseBodyToFile(initialResponse.Body, tempFilePath, initialResponse.ContentRange, 0);
1042
+ const rangeResults = [];
1043
+ rangeResults.push({ index: 0, bytesWritten: responseContentLength });
1044
+ let transferredBytes = responseContentLength;
1045
+ if (emitEvents) {
1046
+ this.dispatchEvent(Object.assign(new Event("bytesTransferred"), {
1047
+ request: initialRangeRequest,
1048
+ snapshot: {
1049
+ transferredBytes,
1050
+ totalBytes: totalContentLength,
1051
+ },
1052
+ }));
1053
+ }
1054
+ const semaphore = new Semaphore(this.maxConcurrentDownloads);
1055
+ const abortController = new AbortController();
1056
+ let failed = false;
1057
+ let failureError;
1058
+ const userSignal = transferOptions?.abortSignal;
1059
+ if (userSignal) {
1060
+ userSignal.addEventListener("abort", () => abortController.abort(), {
1061
+ once: true,
1062
+ });
1063
+ }
1064
+ const rangeTasks = [];
1065
+ let rangeIndex = 1;
1066
+ for (let offset = rangeSize; offset < totalContentLength; offset += rangeSize) {
1067
+ if (failed || abortController.signal.aborted)
1068
+ break;
1069
+ this.checkAborted(transferOptions);
1070
+ await semaphore.acquire();
1071
+ if (failed || abortController.signal.aborted) {
1072
+ semaphore.release();
1073
+ break;
1074
+ }
1075
+ const start = offset;
1076
+ const end = Math.min(offset + rangeSize - 1, totalContentLength - 1);
1077
+ const currentIndex = rangeIndex++;
1078
+ const rangeGetRequest = {
1079
+ ...getObjectFields,
1080
+ Range: `bytes=${start}-${end}`,
1081
+ IfMatch: eTag,
1082
+ ...(checksumValidationEnabled && {
1083
+ ChecksumMode: "ENABLED",
1084
+ }),
1085
+ };
1086
+ const task = (async () => {
1087
+ try {
1088
+ const expectedLength = end - start + 1;
1089
+ if (this.workerHttpHandler) {
1090
+ const resultToken = `download-range-${currentIndex}-${Date.now()}`;
1091
+ const downloadOptions = {
1092
+ filePath: tempFilePath,
1093
+ offset: start,
1094
+ expectedLength,
1095
+ resultToken,
1096
+ };
1097
+ const rangeResponse = await this.s3.send(new GetObjectCommand(rangeGetRequest), {
1098
+ ...transferOptions,
1099
+ downloadDataToFile: downloadOptions,
1100
+ });
1101
+ this.validateRangeDownload(`bytes=${start}-${end}`, rangeResponse.ContentRange);
1102
+ const downloadResult = this.s3.config.requestHandler.getDownloadResult(resultToken);
1103
+ if (!downloadResult) {
1104
+ throw new Error(`Missing download result for range ${start}-${end}`);
1105
+ }
1106
+ rangeResults.push({
1107
+ index: currentIndex,
1108
+ bytesWritten: downloadResult.bytesWritten,
1109
+ });
1110
+ }
1111
+ else {
1112
+ const rangeResponse = await this.s3.send(new GetObjectCommand(rangeGetRequest), transferOptions);
1113
+ this.validateRangeDownload(`bytes=${start}-${end}`, rangeResponse.ContentRange);
1114
+ if (rangeResponse.Body && rangeResponse.Body instanceof Readable) {
1115
+ rangeResponse.Body.on("error", () => { });
1116
+ }
1117
+ await this.writeResponseBodyToFile(rangeResponse.Body, tempFilePath, rangeResponse.ContentRange, start);
1118
+ rangeResults.push({
1119
+ index: currentIndex,
1120
+ bytesWritten: rangeResponse.ContentLength ?? expectedLength,
1121
+ });
1122
+ }
1123
+ transferredBytes += end - start + 1;
1124
+ if (emitEvents) {
1125
+ this.dispatchEvent(Object.assign(new Event("bytesTransferred"), {
1126
+ request: rangeGetRequest,
1127
+ snapshot: {
1128
+ transferredBytes,
1129
+ totalBytes: totalContentLength,
1130
+ },
1131
+ }));
1132
+ }
1133
+ }
1134
+ catch (error) {
1135
+ if (!failed) {
1136
+ failed = true;
1137
+ failureError = error;
1138
+ abortController.abort();
1139
+ }
1140
+ }
1141
+ finally {
1142
+ semaphore.release();
1143
+ }
1144
+ })();
1145
+ rangeTasks.push(task);
1146
+ }
1147
+ await Promise.allSettled(rangeTasks);
1148
+ if (abortController.signal.aborted && !failed) {
1149
+ const abortError = Object.assign(new Error("Transfer aborted."), {
1150
+ name: "AbortError",
1151
+ });
1152
+ await this.handleDownloadCleanup(fileManager, tempFilePath, request, totalContentLength, abortError, emitEvents);
1153
+ throw abortError;
1154
+ }
1155
+ if (failed) {
1156
+ await this.handleDownloadCleanup(fileManager, tempFilePath, request, totalContentLength, failureError, emitEvents);
1157
+ throw failureError;
1158
+ }
1159
+ const actualRequestCount = rangeResults.length;
1160
+ if (actualRequestCount !== expectedRequestCount) {
1161
+ const error = new Error(`Expected ${expectedRequestCount} ranged GET requests but sent ${actualRequestCount}`);
1162
+ await this.handleDownloadCleanup(fileManager, tempFilePath, request, totalContentLength, error, emitEvents);
1163
+ throw error;
1164
+ }
1165
+ const totalBytesWritten = rangeResults.reduce((sum, r) => sum + r.bytesWritten, 0);
1166
+ if (totalBytesWritten !== totalContentLength) {
1167
+ const error = new Error(`Total bytes written (${totalBytesWritten}) does not match expected object size (${totalContentLength})`);
1168
+ await this.handleDownloadCleanup(fileManager, tempFilePath, request, totalContentLength, error, emitEvents);
1169
+ throw error;
1170
+ }
1171
+ await fileManager.atomicRename(tempFilePath, resolvedDestination);
1172
+ fileManager.unregisterCleanupHandler(tempFilePath);
1173
+ return this.buildDownloadToFileResponse(initialResponse, totalContentLength, request, emitEvents);
1174
+ }
1175
+ catch (error) {
1176
+ if (tempFilePath) {
1177
+ await fileManager.deleteTempFile(tempFilePath).catch(() => { });
1178
+ fileManager.unregisterCleanupHandler(tempFilePath);
1179
+ }
1180
+ throw error;
1181
+ }
1182
+ }
1183
+ async writeResponseBodyToFile(body, filePath, contentRange, fallbackOffset) {
1184
+ const start = parseContentRangeStart(contentRange) ?? fallbackOffset;
1185
+ const fd = await open(filePath, "r+");
1186
+ try {
1187
+ let offset = start;
1188
+ if (typeof body[Symbol.asyncIterator] === "function") {
1189
+ for await (const chunk of body) {
1190
+ await fd.write(chunk, 0, chunk.length, offset);
1191
+ offset += chunk.length;
1192
+ }
1193
+ }
1194
+ else if (typeof body.transformToByteArray === "function") {
1195
+ const bytes = await body.transformToByteArray();
1196
+ await fd.write(bytes, 0, bytes.length, offset);
1197
+ offset += bytes.length;
1198
+ }
1199
+ return offset - start;
1200
+ }
1201
+ finally {
1202
+ await fd.close();
1203
+ }
1204
+ }
1205
+ async uploadDirectory(request, transferOptions) {
1206
+ const maxConcurrency = request.maxConcurrency ?? 100;
1207
+ if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
1208
+ throw new Error("maxConcurrency must be a positive integer");
1209
+ }
1210
+ const absoluteSource = await validateDirectory(request.source);
1211
+ const failurePolicy = request.failurePolicy ?? "terminate";
1212
+ const semaphore = new Semaphore(maxConcurrency);
1213
+ const abortController = new AbortController();
1214
+ const inFlight = new Set();
1215
+ let objectsUploaded = 0;
1216
+ let objectsFailed = 0;
1217
+ let terminated = false;
1218
+ let terminationError;
1219
+ this.checkAborted(transferOptions);
1220
+ this.addEventListeners(transferOptions?.eventListeners);
1221
+ let transferredBytes = 0;
1222
+ let transferredFiles = 0;
1223
+ let totalFiles = undefined;
1224
+ let totalBytes = undefined;
1225
+ let traversalComplete = false;
1226
+ const userSignal = transferOptions?.abortSignal;
1227
+ const onUserAbort = () => {
1228
+ if (!terminated) {
1229
+ terminated = true;
1230
+ terminationError = Object.assign(new Error("Transfer aborted."), { name: "AbortError" });
1231
+ }
1232
+ abortController.abort();
1233
+ };
1234
+ const removeLocalEventListeners = () => {
1235
+ this.removeEventListeners(transferOptions?.eventListeners);
1236
+ userSignal?.removeEventListener("abort", onUserAbort);
1237
+ };
1238
+ this.dispatchEvent(Object.assign(new Event("transferInitiated"), {
1239
+ request,
1240
+ snapshot: {
1241
+ transferredBytes: 0,
1242
+ totalBytes: undefined,
1243
+ transferredFiles: 0,
1244
+ totalFiles: undefined,
1245
+ },
1246
+ }));
1247
+ userSignal?.addEventListener("abort", onUserAbort, { once: true });
1248
+ let discoveredFiles = 0;
1249
+ let discoveredBytes = 0;
1250
+ for await (const filePath of this.traverseDirectory(absoluteSource, {
1251
+ recursive: request.recursive ?? false,
1252
+ followSymbolicLinks: request.followSymbolicLinks ?? false,
1253
+ })) {
1254
+ if (terminated || abortController.signal.aborted)
1255
+ break;
1256
+ this.checkAborted(transferOptions);
1257
+ if (request.filter) {
1258
+ const include = request.filter instanceof RegExp ? matchesFilterRegExp(request.filter, filePath) : request.filter(filePath);
1259
+ if (!include)
1260
+ continue;
1261
+ }
1262
+ const fileStat = await stat(filePath);
1263
+ discoveredFiles++;
1264
+ discoveredBytes += fileStat.size;
1265
+ const count = fileStat.size >= this.multipartUploadThresholdBytes
1266
+ ? Math.min(Math.ceil(fileStat.size / this.targetPartSizeBytes), this.maxConcurrentUploads)
1267
+ : 1;
1268
+ await semaphore.acquire(count);
1269
+ if (terminated || abortController.signal.aborted) {
1270
+ semaphore.release(count);
1271
+ break;
1272
+ }
1273
+ const task = (async () => {
1274
+ try {
1275
+ const key = this.deriveS3Key(absoluteSource, filePath, request.s3Prefix);
1276
+ let putRequest = {
1277
+ Bucket: request.bucket,
1278
+ Key: key,
1279
+ Body: createReadStream(filePath),
1280
+ ContentLength: fileStat.size,
1281
+ };
1282
+ if (request.uploadObjectRequestModifier) {
1283
+ putRequest = request.uploadObjectRequestModifier(putRequest);
1284
+ }
1285
+ await this.uploadInternal(putRequest, {
1286
+ abortSignal: abortController.signal,
1287
+ }, false);
1288
+ objectsUploaded++;
1289
+ transferredFiles++;
1290
+ transferredBytes += fileStat.size;
1291
+ this.dispatchEvent(Object.assign(new Event("bytesTransferred"), {
1292
+ request,
1293
+ snapshot: {
1294
+ transferredBytes,
1295
+ totalBytes: traversalComplete ? totalBytes : undefined,
1296
+ transferredFiles,
1297
+ totalFiles: traversalComplete ? totalFiles : undefined,
1298
+ },
1299
+ }));
1300
+ }
1301
+ catch (error) {
1302
+ if (terminated || abortController.signal.aborted) {
1303
+ objectsFailed++;
1304
+ return;
1305
+ }
1306
+ const context = {
1307
+ request,
1308
+ objectRequest: {
1309
+ Bucket: request.bucket,
1310
+ Key: this.deriveS3Key(absoluteSource, filePath, request.s3Prefix),
1311
+ },
1312
+ error,
1313
+ };
1314
+ const result = await handleFailure(failurePolicy, context, abortController);
1315
+ if (result === "terminate") {
1316
+ terminated = true;
1317
+ if (!terminationError) {
1318
+ terminationError = error;
1319
+ }
1320
+ }
1321
+ objectsFailed++;
1322
+ }
1323
+ finally {
1324
+ semaphore.release(count);
1325
+ }
1326
+ })();
1327
+ inFlight.add(task);
1328
+ task.finally(() => inFlight.delete(task));
1329
+ }
1330
+ traversalComplete = true;
1331
+ totalFiles = discoveredFiles;
1332
+ totalBytes = discoveredBytes;
1333
+ await Promise.allSettled([...inFlight]);
1334
+ if (terminated && terminationError) {
1335
+ this.dispatchEvent(Object.assign(new Event("transferFailed"), {
1336
+ request,
1337
+ snapshot: {
1338
+ transferredBytes,
1339
+ totalBytes,
1340
+ transferredFiles,
1341
+ totalFiles,
1342
+ },
1343
+ }));
1344
+ removeLocalEventListeners();
1345
+ throw terminationError;
1346
+ }
1347
+ this.dispatchEvent(Object.assign(new Event("transferComplete"), {
1348
+ request,
1349
+ response: { objectsUploaded, objectsFailed },
1350
+ snapshot: {
1351
+ transferredBytes,
1352
+ totalBytes,
1353
+ transferredFiles,
1354
+ totalFiles,
1355
+ },
1356
+ }));
1357
+ removeLocalEventListeners();
1358
+ return { objectsUploaded, objectsFailed };
1359
+ }
1360
+ async downloadDirectory(request, transferOptions) {
1361
+ const maxConcurrency = request.maxConcurrency ?? 100;
1362
+ if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
1363
+ throw new Error("maxConcurrency must be a positive integer");
1364
+ }
1365
+ const destination = await createDestinationDirectory(request.destination);
1366
+ const failurePolicy = request.failurePolicy ?? "terminate";
1367
+ const semaphore = new Semaphore(maxConcurrency);
1368
+ const abortController = new AbortController();
1369
+ const inFlight = new Set();
1370
+ let objectsDownloaded = 0;
1371
+ let objectsFailed = 0;
1372
+ let terminated = false;
1373
+ let terminationError;
1374
+ this.checkAborted(transferOptions);
1375
+ this.addEventListeners(transferOptions?.eventListeners);
1376
+ let transferredBytes = 0;
1377
+ let transferredFiles = 0;
1378
+ let discoveredFiles = 0;
1379
+ let discoveredBytes = 0;
1380
+ let totalFiles = undefined;
1381
+ let totalBytes = undefined;
1382
+ let traversalComplete = false;
1383
+ const userSignal = transferOptions?.abortSignal;
1384
+ const onUserAbort = () => {
1385
+ if (!terminated) {
1386
+ terminated = true;
1387
+ terminationError = Object.assign(new Error("Transfer aborted."), { name: "AbortError" });
1388
+ }
1389
+ abortController.abort();
1390
+ };
1391
+ const removeLocalEventListeners = () => {
1392
+ this.removeEventListeners(transferOptions?.eventListeners);
1393
+ userSignal?.removeEventListener("abort", onUserAbort);
1394
+ if (transferOptions?.abortSignal) {
1395
+ this.abortCleanupFunctions.get(transferOptions.abortSignal)?.();
1396
+ this.abortCleanupFunctions.delete(transferOptions.abortSignal);
1397
+ }
1398
+ };
1399
+ this.dispatchEvent(Object.assign(new Event("transferInitiated"), {
1400
+ request,
1401
+ snapshot: {
1402
+ transferredBytes: 0,
1403
+ totalBytes: undefined,
1404
+ transferredFiles: 0,
1405
+ totalFiles: undefined,
1406
+ },
1407
+ }));
1408
+ userSignal?.addEventListener("abort", onUserAbort, { once: true });
1409
+ try {
1410
+ let continuationToken;
1411
+ do {
1412
+ if (terminated || abortController.signal.aborted)
1413
+ break;
1414
+ this.checkAborted(transferOptions);
1415
+ const listRequest = {
1416
+ Bucket: request.bucket,
1417
+ Prefix: request.s3Prefix,
1418
+ Delimiter: undefined,
1419
+ ContinuationToken: continuationToken,
1420
+ };
1421
+ const listResponse = await this.s3.send(new ListObjectsV2Command(listRequest), {
1422
+ abortSignal: abortController.signal,
1423
+ });
1424
+ continuationToken = listResponse.IsTruncated ? listResponse.NextContinuationToken : undefined;
1425
+ for (const object of listResponse.Contents ?? []) {
1426
+ if (terminated || abortController.signal.aborted)
1427
+ break;
1428
+ this.checkAborted(transferOptions);
1429
+ if (isFolderObject(object)) {
1430
+ continue;
1431
+ }
1432
+ if (!object.Key) {
1433
+ continue;
1434
+ }
1435
+ if (request.filter) {
1436
+ const include = request.filter instanceof RegExp
1437
+ ? matchesFilterRegExp(request.filter, object.Key)
1438
+ : request.filter(object);
1439
+ if (!include) {
1440
+ continue;
1441
+ }
1442
+ }
1443
+ const currentObject = object;
1444
+ const key = currentObject.Key;
1445
+ await semaphore.acquire();
1446
+ if (terminated || abortController.signal.aborted) {
1447
+ semaphore.release();
1448
+ break;
1449
+ }
1450
+ const task = (async () => {
1451
+ let objectRequest = {
1452
+ Bucket: request.bucket,
1453
+ Key: key,
1454
+ };
1455
+ try {
1456
+ const localPath = deriveLocalPath(destination, key);
1457
+ await createLocalParentDirectories(destination, localPath);
1458
+ if (request.downloadObjectRequestModifier) {
1459
+ objectRequest = request.downloadObjectRequestModifier(objectRequest);
1460
+ }
1461
+ await this.downloadToFileInternal({
1462
+ ...objectRequest,
1463
+ destination: localPath,
1464
+ }, {
1465
+ abortSignal: abortController.signal,
1466
+ }, false);
1467
+ objectsDownloaded++;
1468
+ transferredFiles++;
1469
+ transferredBytes += currentObject.Size ?? 0;
1470
+ this.dispatchEvent(Object.assign(new Event("bytesTransferred"), {
1471
+ request,
1472
+ snapshot: {
1473
+ transferredBytes,
1474
+ totalBytes: traversalComplete ? totalBytes : undefined,
1475
+ transferredFiles,
1476
+ totalFiles: traversalComplete ? totalFiles : undefined,
1477
+ },
1478
+ }));
1479
+ }
1480
+ catch (error) {
1481
+ if (terminated || abortController.signal.aborted) {
1482
+ objectsFailed++;
1483
+ return;
1484
+ }
1485
+ const context = {
1486
+ request,
1487
+ objectRequest,
1488
+ error,
1489
+ };
1490
+ const result = await handleFailure(failurePolicy, context, abortController);
1491
+ if (result === "terminate") {
1492
+ terminated = true;
1493
+ if (!terminationError) {
1494
+ terminationError = error;
1495
+ }
1496
+ }
1497
+ objectsFailed++;
1498
+ }
1499
+ finally {
1500
+ semaphore.release();
1501
+ }
1502
+ })();
1503
+ inFlight.add(task);
1504
+ task.finally(() => inFlight.delete(task));
1505
+ discoveredFiles++;
1506
+ discoveredBytes += currentObject.Size ?? 0;
1507
+ }
1508
+ } while (continuationToken);
1509
+ }
1510
+ catch (error) {
1511
+ if (!terminated) {
1512
+ terminated = true;
1513
+ terminationError = error;
1514
+ }
1515
+ abortController.abort();
1516
+ }
1517
+ traversalComplete = true;
1518
+ totalFiles = discoveredFiles;
1519
+ totalBytes = discoveredBytes;
1520
+ await Promise.allSettled([...inFlight]);
1521
+ if (terminated && terminationError) {
1522
+ this.dispatchEvent(Object.assign(new Event("transferFailed"), {
1523
+ request,
1524
+ snapshot: {
1525
+ transferredBytes,
1526
+ totalBytes,
1527
+ transferredFiles,
1528
+ totalFiles,
1529
+ },
1530
+ }));
1531
+ removeLocalEventListeners();
1532
+ throw terminationError;
1533
+ }
1534
+ this.dispatchEvent(Object.assign(new Event("transferComplete"), {
1535
+ request,
1536
+ response: { objectsDownloaded, objectsFailed },
1537
+ snapshot: {
1538
+ transferredBytes,
1539
+ totalBytes,
1540
+ transferredFiles,
1541
+ totalFiles,
1542
+ },
1543
+ }));
1544
+ removeLocalEventListeners();
1545
+ return { objectsDownloaded, objectsFailed };
1546
+ }
1547
+ async downloadByPart(request, transferOptions, streams, requests, metadata, checksumValidationEnabled) {
1548
+ let totalSize;
1549
+ let streamConsumedCallback;
1550
+ this.checkAborted(transferOptions);
1551
+ if (request.Range == null) {
1552
+ const initialPartRequest = {
1553
+ ...request,
1554
+ PartNumber: 1,
1555
+ ...(checksumValidationEnabled && { ChecksumMode: "ENABLED" }),
1556
+ };
1557
+ try {
1558
+ const initialPart = await this.s3.send(new GetObjectCommand(initialPartRequest), transferOptions);
1559
+ await internalEventHandler.afterInitialGetObject();
1560
+ const partSize = initialPart.ContentLength;
1561
+ const initialETag = initialPart.ETag ?? undefined;
1562
+ totalSize = initialPart.ContentRange ? Number.parseInt(initialPart.ContentRange.split("/")[1]) : 0;
1563
+ this.dispatchTransferInitiatedEvent(request, totalSize);
1564
+ if (initialPart.Body) {
1565
+ if (initialPart.Body && typeof initialPart.Body.getReader === "function") {
1566
+ const reader = initialPart.Body.getReader();
1567
+ initialPart.Body.getReader = function () {
1568
+ return reader;
1569
+ };
1570
+ }
1571
+ streams.push(Promise.resolve(initialPart.Body));
1572
+ requests.push(initialPartRequest);
1573
+ }
1574
+ this.processResponseMetadata(initialPart, metadata, totalSize);
1575
+ let partCount = 1;
1576
+ if (initialPart.PartsCount > 1) {
1577
+ const partTasks = [];
1578
+ const partRequests = [];
1579
+ for (let part = 2; part <= initialPart.PartsCount; part++) {
1580
+ const getObjectRequest = {
1581
+ ...request,
1582
+ PartNumber: part,
1583
+ IfMatch: initialETag,
1584
+ ...(checksumValidationEnabled && {
1585
+ ChecksumMode: "ENABLED",
1586
+ }),
1587
+ };
1588
+ partRequests.push(getObjectRequest);
1589
+ partTasks.push(() => {
1590
+ this.checkAborted(transferOptions);
1591
+ return this.s3
1592
+ .send(new GetObjectCommand(getObjectRequest), transferOptions)
1593
+ .then((response) => {
1594
+ this.validatePartDownload(response.ContentRange, part, partSize ?? 0);
1595
+ if (response.Body && typeof response.Body.getReader === "function") {
1596
+ const reader = response.Body.getReader();
1597
+ response.Body.getReader = function () {
1598
+ return reader;
1599
+ };
1600
+ }
1601
+ return response.Body;
1602
+ })
1603
+ .catch((error) => {
1604
+ this.dispatchTransferFailedEvent(getObjectRequest, totalSize, error);
1605
+ throw error;
1606
+ });
1607
+ });
1608
+ }
1609
+ const { promises: pendingStreams, onStreamConsumed } = this.createConcurrentTaskPool(partTasks, this.maxConcurrentDownloads);
1610
+ streams.push(...pendingStreams);
1611
+ requests.push(...partRequests);
1612
+ streamConsumedCallback = onStreamConsumed;
1613
+ partCount += partTasks.length;
1614
+ if (partCount !== initialPart.PartsCount) {
1615
+ throw new Error(`The number of parts downloaded (${partCount}) does not match the expected number (${initialPart.PartsCount})`);
1616
+ }
1617
+ }
1618
+ }
1619
+ catch (error) {
1620
+ this.dispatchTransferFailedEvent(request, totalSize, error);
1621
+ throw error;
1622
+ }
1623
+ }
1624
+ else {
1625
+ this.checkAborted(transferOptions);
1626
+ try {
1627
+ const getObjectRequest = {
1628
+ ...request,
1629
+ ...(checksumValidationEnabled && {
1630
+ ChecksumMode: "ENABLED",
1631
+ }),
1632
+ };
1633
+ const getObject = await this.s3.send(new GetObjectCommand(getObjectRequest), transferOptions);
1634
+ totalSize = getObject.ContentRange ? Number.parseInt(getObject.ContentRange.split("/")[1]) : 0;
1635
+ this.dispatchTransferInitiatedEvent(request, totalSize);
1636
+ if (getObject.Body) {
1637
+ streams.push(Promise.resolve(getObject.Body));
1638
+ requests.push(getObjectRequest);
1639
+ }
1640
+ this.processResponseMetadata(getObject, metadata, totalSize);
1641
+ }
1642
+ catch (error) {
1643
+ this.dispatchTransferFailedEvent(request, undefined, error);
1644
+ throw error;
1645
+ }
1646
+ }
1647
+ return {
1648
+ totalSize,
1649
+ onStreamConsumed: streamConsumedCallback,
1650
+ };
1651
+ }
1652
+ async downloadByRange(request, transferOptions, streams, requests, metadata, checksumValidationEnabled) {
1653
+ let streamConsumedCallback;
1654
+ this.checkAborted(transferOptions);
1655
+ const headResponse = await this.s3.send(new HeadObjectCommand({ Bucket: request.Bucket, Key: request.Key }), transferOptions);
1656
+ if (headResponse.ContentLength === 0) {
1657
+ const getObjectRequest = {
1658
+ ...request,
1659
+ ...(checksumValidationEnabled && { ChecksumMode: "ENABLED" }),
1660
+ };
1661
+ const response = await this.s3.send(new GetObjectCommand(getObjectRequest), transferOptions);
1662
+ this.dispatchTransferInitiatedEvent(request, 0);
1663
+ if (response.Body)
1664
+ streams.push(Promise.resolve(response.Body));
1665
+ requests.push(getObjectRequest);
1666
+ this.processResponseMetadata(response, metadata, 0);
1667
+ return { totalSize: 0 };
1668
+ }
1669
+ let left = 0;
1670
+ let right = this.targetPartSizeBytes - 1;
1671
+ let maxRange = Number.POSITIVE_INFINITY;
1672
+ let remainingLength = 1;
1673
+ let totalSize;
1674
+ let initialETag;
1675
+ if (request.Range != null) {
1676
+ const [userRangeLeft, userRangeRight] = request.Range.replace("bytes=", "").split("-").map(Number);
1677
+ maxRange = userRangeRight;
1678
+ left = userRangeLeft;
1679
+ right = Math.min(userRangeRight, left + S3TransferManagerBase.MIN_PART_SIZE - 1);
1680
+ totalSize = userRangeRight + 1;
1681
+ }
1682
+ try {
1683
+ const getObjectRequest = {
1684
+ ...request,
1685
+ Range: `bytes=${left}-${right}`,
1686
+ ...(checksumValidationEnabled && { ChecksumMode: "ENABLED" }),
1687
+ };
1688
+ const initialRangeGet = await this.s3.send(new GetObjectCommand(getObjectRequest), transferOptions);
1689
+ await internalEventHandler.afterInitialGetObject();
1690
+ this.validateRangeDownload(`bytes=${left}-${right}`, initialRangeGet.ContentRange);
1691
+ initialETag = initialRangeGet.ETag ?? undefined;
1692
+ if (!totalSize) {
1693
+ totalSize = initialRangeGet.ContentRange
1694
+ ? Number.parseInt(initialRangeGet.ContentRange.split("/")[1])
1695
+ : undefined;
1696
+ }
1697
+ if (initialRangeGet.Body && typeof initialRangeGet.Body.getReader === "function") {
1698
+ const reader = initialRangeGet.Body.getReader();
1699
+ initialRangeGet.Body.getReader = function () {
1700
+ return reader;
1701
+ };
1702
+ }
1703
+ this.dispatchTransferInitiatedEvent(request, totalSize);
1704
+ streams.push(Promise.resolve(initialRangeGet.Body));
1705
+ requests.push(getObjectRequest);
1706
+ this.processResponseMetadata(initialRangeGet, metadata, totalSize);
1707
+ }
1708
+ catch (error) {
1709
+ this.dispatchTransferFailedEvent(request, totalSize, error);
1710
+ throw error;
1711
+ }
1712
+ let expectedRequestCount = 1;
1713
+ if (totalSize) {
1714
+ const contentLength = totalSize;
1715
+ const remainingBytes = Math.max(0, contentLength - (right - left + 1));
1716
+ const additionalRequests = Math.ceil(remainingBytes / S3TransferManagerBase.MIN_PART_SIZE);
1717
+ expectedRequestCount += additionalRequests;
1718
+ }
1719
+ left = right + 1;
1720
+ right = Math.min(left + S3TransferManagerBase.MIN_PART_SIZE - 1, maxRange);
1721
+ remainingLength = totalSize ? Math.min(right - left + 1, Math.max(0, totalSize - left)) : 0;
1722
+ const rangeTasks = [];
1723
+ const rangeRequests = [];
1724
+ while (remainingLength > 0) {
1725
+ const range = `bytes=${left}-${right}`;
1726
+ const getObjectRequest = {
1727
+ ...request,
1728
+ Range: range,
1729
+ IfMatch: initialETag,
1730
+ ...(checksumValidationEnabled && { ChecksumMode: "ENABLED" }),
1731
+ };
1732
+ rangeRequests.push(getObjectRequest);
1733
+ rangeTasks.push(() => {
1734
+ this.checkAborted(transferOptions);
1735
+ return this.s3
1736
+ .send(new GetObjectCommand(getObjectRequest), transferOptions)
1737
+ .then((response) => {
1738
+ this.validateRangeDownload(range, response.ContentRange);
1739
+ if (response.Body && typeof response.Body.getReader === "function") {
1740
+ const reader = response.Body.getReader();
1741
+ response.Body.getReader = function () {
1742
+ return reader;
1743
+ };
1744
+ }
1745
+ return response.Body;
1746
+ })
1747
+ .catch((error) => {
1748
+ this.dispatchTransferFailedEvent(getObjectRequest, totalSize, error);
1749
+ throw error;
1750
+ });
1751
+ });
1752
+ left = right + 1;
1753
+ right = Math.min(left + S3TransferManagerBase.MIN_PART_SIZE - 1, maxRange);
1754
+ remainingLength = totalSize ? Math.min(right - left + 1, Math.max(0, totalSize - left)) : 0;
1755
+ }
1756
+ if (rangeTasks.length > 0) {
1757
+ const { promises: pendingStreams, onStreamConsumed } = this.createConcurrentTaskPool(rangeTasks, this.maxConcurrentDownloads);
1758
+ streams.push(...pendingStreams);
1759
+ requests.push(...rangeRequests);
1760
+ streamConsumedCallback = onStreamConsumed;
1761
+ }
1762
+ const actualRequestCount = 1 + rangeTasks.length;
1763
+ if (expectedRequestCount !== actualRequestCount) {
1764
+ throw new Error(`The number of ranged GET requests sent (${actualRequestCount}) does not match the expected number (${expectedRequestCount})`);
1765
+ }
1766
+ return {
1767
+ totalSize,
1768
+ onStreamConsumed: streamConsumedCallback,
1769
+ };
1770
+ }
1771
+ async downloadByPartWithWorkers(request, transferOptions, streams, requests, metadata, checksumValidationEnabled) {
1772
+ this.checkAborted(transferOptions);
1773
+ const { Range: _range, ...requestWithoutRange } = request;
1774
+ const initialPartRequest = {
1775
+ ...requestWithoutRange,
1776
+ PartNumber: 1,
1777
+ ...(checksumValidationEnabled && { ChecksumMode: "ENABLED" }),
1778
+ };
1779
+ const initialPart = await this.s3.send(new GetObjectCommand(initialPartRequest), transferOptions);
1780
+ await internalEventHandler.afterInitialGetObject();
1781
+ const partSize = initialPart.ContentLength;
1782
+ const initialETag = initialPart.ETag ?? undefined;
1783
+ const totalSize = initialPart.ContentRange
1784
+ ? Number.parseInt(initialPart.ContentRange.split("/")[1])
1785
+ : (initialPart.ContentLength ?? 0);
1786
+ const partsCount = initialPart.PartsCount ?? 1;
1787
+ this.dispatchTransferInitiatedEvent(request, totalSize);
1788
+ if (initialPart.Body) {
1789
+ if (typeof initialPart.Body.getReader === "function") {
1790
+ const reader = initialPart.Body.getReader();
1791
+ initialPart.Body.getReader = function () {
1792
+ return reader;
1793
+ };
1794
+ }
1795
+ streams.push(Promise.resolve(initialPart.Body));
1796
+ requests.push(initialPartRequest);
1797
+ }
1798
+ this.processResponseMetadata(initialPart, metadata, totalSize);
1799
+ if (partsCount <= 1) {
1800
+ return { totalSize };
1801
+ }
1802
+ this.checkAborted(transferOptions);
1803
+ const remainingParts = partsCount - 1;
1804
+ const queue = new OrderedPartQueue(remainingParts);
1805
+ const abortController = new AbortController();
1806
+ const userSignal = transferOptions?.abortSignal;
1807
+ if (userSignal) {
1808
+ userSignal.addEventListener("abort", () => abortController.abort(), {
1809
+ once: true,
1810
+ });
1811
+ }
1812
+ let inFlight = 0;
1813
+ let nextDispatchResolve = null;
1814
+ const waitForSlot = () => {
1815
+ if (inFlight < this.maxConcurrentDownloads) {
1816
+ return Promise.resolve();
1817
+ }
1818
+ return new Promise((resolve) => {
1819
+ nextDispatchResolve = resolve;
1820
+ });
1821
+ };
1822
+ const releaseSlot = () => {
1823
+ if (inFlight <= 0) {
1824
+ throw new Error("releaseSlot called with no in-flight requests");
1825
+ }
1826
+ inFlight--;
1827
+ if (nextDispatchResolve && inFlight < this.maxConcurrentDownloads) {
1828
+ const resolve = nextDispatchResolve;
1829
+ nextDispatchResolve = null;
1830
+ resolve();
1831
+ }
1832
+ };
1833
+ (async () => {
1834
+ for (let partNumber = 2; partNumber <= partsCount; partNumber++) {
1835
+ if (abortController.signal.aborted || queue.hasError())
1836
+ break;
1837
+ this.checkAborted(transferOptions);
1838
+ await waitForSlot();
1839
+ if (abortController.signal.aborted || queue.hasError())
1840
+ break;
1841
+ inFlight++;
1842
+ const rangeIndex = partNumber - 2;
1843
+ const partGetRequest = {
1844
+ ...requestWithoutRange,
1845
+ PartNumber: partNumber,
1846
+ IfMatch: initialETag,
1847
+ ...(checksumValidationEnabled && {
1848
+ ChecksumMode: "ENABLED",
1849
+ }),
1850
+ };
1851
+ requests.push(partGetRequest);
1852
+ const resultToken = `download-transfer-part-${rangeIndex}-${Date.now()}`;
1853
+ const transferOptions_ = {
1854
+ expectedSize: partSize,
1855
+ rangeIndex,
1856
+ checksumAlgorithm: checksumValidationEnabled ? "CRC32" : undefined,
1857
+ resultToken,
1858
+ };
1859
+ this.s3
1860
+ .send(new GetObjectCommand(partGetRequest), {
1861
+ ...transferOptions,
1862
+ downloadStream: transferOptions_,
1863
+ })
1864
+ .then(() => {
1865
+ const transferResult = this.s3.config.requestHandler.getStreamDownloadResult(resultToken);
1866
+ if (!transferResult) {
1867
+ queue.setError(new Error(`Missing download result for part ${partNumber}`));
1868
+ abortController.abort();
1869
+ return;
1870
+ }
1871
+ this.validatePartDownload(transferResult.headers["content-range"], partNumber, partSize);
1872
+ queue.enqueue(rangeIndex, transferResult.buffer, transferResult.byteLength);
1873
+ })
1874
+ .catch((error) => {
1875
+ queue.setError(error);
1876
+ abortController.abort();
1877
+ })
1878
+ .finally(() => {
1879
+ releaseSlot();
1880
+ });
1881
+ }
1882
+ })().catch((err) => {
1883
+ if (!queue.hasError()) {
1884
+ queue.setError(err);
1885
+ }
1886
+ });
1887
+ let reading = false;
1888
+ const transferStream = new Readable({
1889
+ read() {
1890
+ if (reading)
1891
+ return;
1892
+ reading = true;
1893
+ queue
1894
+ .dequeue()
1895
+ .then((item) => {
1896
+ reading = false;
1897
+ if (item === null) {
1898
+ if (queue.hasError()) {
1899
+ this.destroy(queue.getError());
1900
+ return;
1901
+ }
1902
+ this.push(null);
1903
+ return;
1904
+ }
1905
+ const buf = Buffer.from(item.buffer, 0, item.byteLength);
1906
+ this.push(buf);
1907
+ })
1908
+ .catch((err) => {
1909
+ reading = false;
1910
+ this.destroy(err);
1911
+ });
1912
+ },
1913
+ destroy(err, callback) {
1914
+ if (err) {
1915
+ queue.setError(err);
1916
+ abortController.abort();
1917
+ }
1918
+ callback(err);
1919
+ },
1920
+ });
1921
+ streams.push(Promise.resolve(transferStream));
1922
+ return { totalSize };
1923
+ }
1924
+ async downloadByRangeWithWorkers(request, transferOptions, streams, requests, metadata, checksumValidationEnabled) {
1925
+ this.checkAborted(transferOptions);
1926
+ const headResponse = await this.s3.send(new HeadObjectCommand({ Bucket: request.Bucket, Key: request.Key }), transferOptions);
1927
+ await internalEventHandler.afterInitialGetObject();
1928
+ if (headResponse.ContentLength === 0) {
1929
+ const getObjectRequest = {
1930
+ ...request,
1931
+ ...(checksumValidationEnabled && { ChecksumMode: "ENABLED" }),
1932
+ };
1933
+ const response = await this.s3.send(new GetObjectCommand(getObjectRequest), transferOptions);
1934
+ this.dispatchTransferInitiatedEvent(request, 0);
1935
+ if (response.Body)
1936
+ streams.push(Promise.resolve(response.Body));
1937
+ requests.push(getObjectRequest);
1938
+ this.processResponseMetadata(response, metadata, 0);
1939
+ return { totalSize: 0 };
1940
+ }
1941
+ const totalSize = headResponse.ContentLength;
1942
+ const partSize = this.targetPartSizeBytes;
1943
+ const totalParts = Math.ceil(totalSize / partSize);
1944
+ const eTag = headResponse.ETag ?? undefined;
1945
+ this.dispatchTransferInitiatedEvent(request, totalSize);
1946
+ Object.assign(metadata, {
1947
+ ContentLength: totalSize,
1948
+ ContentRange: `bytes 0-${totalSize - 1}/${totalSize}`,
1949
+ ContentType: headResponse.ContentType,
1950
+ ETag: headResponse.ETag,
1951
+ LastModified: headResponse.LastModified,
1952
+ $metadata: headResponse.$metadata,
1953
+ });
1954
+ const queue = new OrderedPartQueue(totalParts);
1955
+ const abortController = new AbortController();
1956
+ const userSignal = transferOptions?.abortSignal;
1957
+ if (userSignal) {
1958
+ userSignal.addEventListener("abort", () => abortController.abort(), {
1959
+ once: true,
1960
+ });
1961
+ }
1962
+ let inFlight = 0;
1963
+ let nextDispatchResolve = null;
1964
+ const waitForSlot = () => {
1965
+ if (inFlight < this.maxConcurrentDownloads) {
1966
+ return Promise.resolve();
1967
+ }
1968
+ return new Promise((resolve) => {
1969
+ nextDispatchResolve = resolve;
1970
+ });
1971
+ };
1972
+ const releaseSlot = () => {
1973
+ if (inFlight <= 0) {
1974
+ throw new Error("releaseSlot called with no in-flight requests");
1975
+ }
1976
+ inFlight--;
1977
+ if (nextDispatchResolve && inFlight < this.maxConcurrentDownloads) {
1978
+ const resolve = nextDispatchResolve;
1979
+ nextDispatchResolve = null;
1980
+ resolve();
1981
+ }
1982
+ };
1983
+ (async () => {
1984
+ for (let rangeIndex = 0; rangeIndex < totalParts; rangeIndex++) {
1985
+ if (abortController.signal.aborted || queue.hasError())
1986
+ break;
1987
+ this.checkAborted(transferOptions);
1988
+ await waitForSlot();
1989
+ if (abortController.signal.aborted || queue.hasError())
1990
+ break;
1991
+ inFlight++;
1992
+ const start = rangeIndex * partSize;
1993
+ const end = Math.min(start + partSize - 1, totalSize - 1);
1994
+ const expectedLength = end - start + 1;
1995
+ const rangeGetRequest = {
1996
+ ...request,
1997
+ Range: `bytes=${start}-${end}`,
1998
+ IfMatch: eTag,
1999
+ ...(checksumValidationEnabled && {
2000
+ ChecksumMode: "ENABLED",
2001
+ }),
2002
+ };
2003
+ requests.push(rangeGetRequest);
2004
+ const resultToken = `download-transfer-range-${rangeIndex}-${Date.now()}`;
2005
+ const transferOpts = {
2006
+ expectedSize: expectedLength,
2007
+ rangeIndex,
2008
+ checksumAlgorithm: checksumValidationEnabled ? "CRC32" : undefined,
2009
+ resultToken,
2010
+ };
2011
+ this.s3
2012
+ .send(new GetObjectCommand(rangeGetRequest), {
2013
+ ...transferOptions,
2014
+ downloadStream: transferOpts,
2015
+ })
2016
+ .then(() => {
2017
+ const transferResult = this.s3.config.requestHandler.getStreamDownloadResult(resultToken);
2018
+ if (!transferResult) {
2019
+ queue.setError(new Error(`Missing download result for range ${start}-${end}`));
2020
+ abortController.abort();
2021
+ return;
2022
+ }
2023
+ this.validateRangeDownload(`bytes=${start}-${end}`, transferResult.headers["content-range"]);
2024
+ queue.enqueue(rangeIndex, transferResult.buffer, transferResult.byteLength);
2025
+ })
2026
+ .catch((error) => {
2027
+ queue.setError(error);
2028
+ abortController.abort();
2029
+ })
2030
+ .finally(() => {
2031
+ releaseSlot();
2032
+ });
2033
+ }
2034
+ })().catch((err) => {
2035
+ if (!queue.hasError()) {
2036
+ queue.setError(err);
2037
+ }
2038
+ });
2039
+ let reading = false;
2040
+ const transferStream = new Readable({
2041
+ read() {
2042
+ if (reading)
2043
+ return;
2044
+ reading = true;
2045
+ queue
2046
+ .dequeue()
2047
+ .then((item) => {
2048
+ reading = false;
2049
+ if (item === null) {
2050
+ if (queue.hasError()) {
2051
+ this.destroy(queue.getError());
2052
+ return;
2053
+ }
2054
+ this.push(null);
2055
+ return;
2056
+ }
2057
+ const buf = Buffer.from(item.buffer, 0, item.byteLength);
2058
+ this.push(buf);
2059
+ })
2060
+ .catch((err) => {
2061
+ reading = false;
2062
+ this.destroy(err);
2063
+ });
2064
+ },
2065
+ destroy(err, callback) {
2066
+ if (err) {
2067
+ queue.setError(err);
2068
+ abortController.abort();
2069
+ }
2070
+ callback(err);
2071
+ },
2072
+ });
2073
+ streams.push(Promise.resolve(transferStream));
2074
+ return { totalSize };
2075
+ }
2076
+ addEventListeners(eventListeners) {
2077
+ for (const listeners of this.iterateListeners(eventListeners)) {
2078
+ for (const listener of listeners) {
2079
+ this.addEventListener(listener.eventType, listener.callback);
2080
+ }
2081
+ }
2082
+ }
2083
+ removeEventListeners(eventListeners) {
2084
+ for (const listeners of this.iterateListeners(eventListeners)) {
2085
+ for (const listener of listeners) {
2086
+ this.removeEventListener(listener.eventType, listener.callback);
2087
+ }
2088
+ }
2089
+ }
2090
+ assignMetadata(container, response) {
2091
+ for (const key in response) {
2092
+ if (key === "Body") {
2093
+ continue;
2094
+ }
2095
+ container[key] = response[key];
2096
+ }
2097
+ }
2098
+ buildDownloadToFileResponse(initialResponse, totalSize, request, emitEvents) {
2099
+ const metadata = {};
2100
+ this.assignMetadata(metadata, initialResponse);
2101
+ metadata.ContentLength = totalSize;
2102
+ metadata.ContentRange = `bytes 0-${totalSize - 1}/${totalSize}`;
2103
+ if (initialResponse.ChecksumType === "COMPOSITE") {
2104
+ metadata.ChecksumCRC32 = undefined;
2105
+ metadata.ChecksumCRC32C = undefined;
2106
+ metadata.ChecksumSHA1 = undefined;
2107
+ metadata.ChecksumSHA256 = undefined;
2108
+ }
2109
+ const response = {
2110
+ ...metadata,
2111
+ bytesWritten: totalSize,
2112
+ };
2113
+ if (emitEvents) {
2114
+ this.dispatchEvent(Object.assign(new Event("transferComplete"), {
2115
+ request,
2116
+ response,
2117
+ snapshot: {
2118
+ transferredBytes: totalSize,
2119
+ totalBytes: totalSize,
2120
+ },
2121
+ }));
2122
+ }
2123
+ return response;
2124
+ }
2125
+ async handleDownloadCleanup(fileManager, tempFilePath, request, totalSize, error, emitEvents) {
2126
+ await fileManager.deleteTempFile(tempFilePath);
2127
+ fileManager.unregisterCleanupHandler(tempFilePath);
2128
+ if (emitEvents) {
2129
+ this.dispatchTransferFailedEvent(request, totalSize, error);
2130
+ }
2131
+ }
2132
+ updateResponseLengthAndRange(response, totalSize) {
2133
+ if (totalSize !== undefined) {
2134
+ response.ContentLength = totalSize;
2135
+ response.ContentRange = `bytes 0-${totalSize - 1}/${totalSize}`;
2136
+ }
2137
+ }
2138
+ updateChecksumValues(initialPart, metadata) {
2139
+ if (initialPart.ChecksumType === "COMPOSITE") {
2140
+ metadata.ChecksumCRC32 = undefined;
2141
+ metadata.ChecksumCRC32C = undefined;
2142
+ metadata.ChecksumSHA1 = undefined;
2143
+ metadata.ChecksumSHA256 = undefined;
2144
+ }
2145
+ }
2146
+ processResponseMetadata(response, metadata, totalSize) {
2147
+ this.updateResponseLengthAndRange(response, totalSize);
2148
+ this.assignMetadata(metadata, response);
2149
+ this.updateChecksumValues(response, metadata);
2150
+ }
2151
+ checkAborted(transferOptions) {
2152
+ if (transferOptions?.abortSignal?.aborted) {
2153
+ throw Object.assign(new Error("Transfer aborted."), {
2154
+ name: "AbortError",
2155
+ });
2156
+ }
2157
+ }
2158
+ validateConfig() {
2159
+ if (this.targetPartSizeBytes < S3TransferManagerBase.MIN_PART_SIZE) {
2160
+ throw new Error(`targetPartSizeBytes must be at least ${S3TransferManagerBase.MIN_PART_SIZE} bytes`);
2161
+ }
2162
+ }
2163
+ dispatchTransferInitiatedEvent(request, totalSize) {
2164
+ this.dispatchEvent(Object.assign(new Event("transferInitiated"), {
2165
+ request,
2166
+ snapshot: {
2167
+ transferredBytes: 0,
2168
+ totalBytes: totalSize,
2169
+ },
2170
+ }));
2171
+ return true;
2172
+ }
2173
+ dispatchTransferFailedEvent(request, totalSize, error) {
2174
+ this.dispatchEvent(Object.assign(new Event("transferFailed"), {
2175
+ request,
2176
+ error,
2177
+ snapshot: {
2178
+ transferredBytes: 0,
2179
+ totalBytes: totalSize,
2180
+ },
2181
+ }));
2182
+ return true;
2183
+ }
2184
+ *iterateListeners(eventListeners = {}) {
2185
+ for (const key in eventListeners) {
2186
+ const eventType = key;
2187
+ const listeners = eventListeners[eventType];
2188
+ if (Array.isArray(listeners)) {
2189
+ for (const callback of listeners) {
2190
+ yield [
2191
+ {
2192
+ eventType: eventType,
2193
+ callback: callback,
2194
+ },
2195
+ ];
2196
+ }
2197
+ }
2198
+ }
2199
+ }
2200
+ createConcurrentTaskPool(tasks, maxConcurrent) {
2201
+ if (tasks.length === 0)
2202
+ return { promises: [], onStreamConsumed: () => { } };
2203
+ const resolvers = [];
2204
+ const promises = tasks.map(() => {
2205
+ let resolve;
2206
+ let reject;
2207
+ const promise = new Promise((res, rej) => {
2208
+ resolve = res;
2209
+ reject = rej;
2210
+ });
2211
+ resolvers.push({ resolve, reject });
2212
+ return promise;
2213
+ });
2214
+ let nextToLaunch = 0;
2215
+ let failed = false;
2216
+ const launchTask = (index) => {
2217
+ tasks[index]().then((result) => {
2218
+ resolvers[index].resolve(result);
2219
+ }, (error) => {
2220
+ failed = true;
2221
+ resolvers[index].reject(error);
2222
+ });
2223
+ };
2224
+ const initialBatch = Math.min(maxConcurrent, tasks.length);
2225
+ for (let i = 0; i < initialBatch; i++) {
2226
+ launchTask(nextToLaunch++);
2227
+ }
2228
+ const onStreamConsumed = (_index) => {
2229
+ if (failed || nextToLaunch >= tasks.length)
2230
+ return;
2231
+ launchTask(nextToLaunch++);
2232
+ };
2233
+ return { promises, onStreamConsumed };
2234
+ }
2235
+ validatePartDownload(contentRange, partNumber, partSize) {
2236
+ if (!contentRange) {
2237
+ throw new Error(`Missing ContentRange for part ${partNumber}.`);
2238
+ }
2239
+ const match = contentRange.match(/bytes (\d+)-(\d+)\/(\d+)/);
2240
+ if (!match)
2241
+ throw new Error(`Invalid ContentRange format: ${contentRange}`);
2242
+ const start = Number.parseInt(match[1]);
2243
+ const end = Number.parseInt(match[2]);
2244
+ const total = Number.parseInt(match[3]) - 1;
2245
+ const expectedStart = (partNumber - 1) * partSize;
2246
+ const expectedEnd = Math.min(expectedStart + partSize - 1, total);
2247
+ if (start !== expectedStart) {
2248
+ throw new Error(`Expected part ${partNumber} to start at ${expectedStart} but got ${start}`);
2249
+ }
2250
+ if (end !== expectedEnd) {
2251
+ throw new Error(`Expected part ${partNumber} to end at ${expectedEnd} but got ${end}`);
2252
+ }
2253
+ }
2254
+ validateRangeDownload(requestRange, responseRange) {
2255
+ if (!responseRange) {
2256
+ throw new Error(`Missing ContentRange for range ${requestRange}.`);
2257
+ }
2258
+ const match = responseRange.match(/bytes (\d+)-(\d+)\/(\d+)/);
2259
+ if (!match)
2260
+ throw new Error(`Invalid ContentRange format: ${responseRange}`);
2261
+ const start = Number.parseInt(match[1]);
2262
+ const end = Number.parseInt(match[2]);
2263
+ const total = Number.parseInt(match[3]) - 1;
2264
+ const rangeMatch = requestRange.match(/bytes=(\d+)-(\d+)/);
2265
+ if (!rangeMatch)
2266
+ throw new Error(`Invalid Range format: ${requestRange}`);
2267
+ const expectedStart = Number.parseInt(rangeMatch[1]);
2268
+ const expectedEnd = Number.parseInt(rangeMatch[2]);
2269
+ if (start !== expectedStart) {
2270
+ throw new Error(`Expected range to start at ${expectedStart} but got ${start}`);
2271
+ }
2272
+ if (end === expectedEnd) {
2273
+ return;
2274
+ }
2275
+ if (end === total) {
2276
+ return;
2277
+ }
2278
+ throw new Error(`Expected range to end at ${expectedEnd} but got ${end}`);
2279
+ }
2280
+ async uploadSinglePart(request, contentLength, transferOptions, emitEvents) {
2281
+ const putObjectRequest = { ...request };
2282
+ this.checkAborted(transferOptions);
2283
+ const response = await this.s3.send(new PutObjectCommand(putObjectRequest), transferOptions);
2284
+ if (emitEvents) {
2285
+ this.dispatchEvent(Object.assign(new Event("bytesTransferred"), {
2286
+ request,
2287
+ snapshot: {
2288
+ transferredBytes: contentLength,
2289
+ totalBytes: contentLength,
2290
+ },
2291
+ }));
2292
+ }
2293
+ return response;
2294
+ }
2295
+ calculatePartSize(contentLength) {
2296
+ const partSize = Math.max(this.targetPartSizeBytes, Math.ceil(contentLength / 10_000));
2297
+ const expectedPartsCount = Math.ceil(contentLength / partSize);
2298
+ return { partSize, expectedPartsCount };
2299
+ }
2300
+ isFileBody(body) {
2301
+ return typeof body?.pipe === "function" && typeof body.path === "string";
2302
+ }
2303
+ isInMemoryBody(body) {
2304
+ return body instanceof Uint8Array || typeof body === "string";
2305
+ }
2306
+ async threadedUploadInPartsFromFile(request, contentLength, transferOptions, emitEvents) {
2307
+ const { partSize } = this.calculatePartSize(contentLength);
2308
+ const body = request.Body;
2309
+ const filePath = body.path;
2310
+ const startOffset = typeof body.start === "number" ? body.start : 0;
2311
+ const buildDataSource = (checksumAlgorithm, checksumHeader) => ({
2312
+ type: "file",
2313
+ filePath,
2314
+ partSize,
2315
+ totalFileSize: contentLength,
2316
+ startOffset,
2317
+ checksumAlgorithm,
2318
+ checksumHeader,
2319
+ });
2320
+ return this.threadedMultipartUpload(request, contentLength, buildDataSource, transferOptions, emitEvents);
2321
+ }
2322
+ async threadedUploadInParts(request, contentLength, transferOptions, emitEvents) {
2323
+ const { partSize } = this.calculatePartSize(contentLength);
2324
+ const body = request.Body;
2325
+ const isSharedBufferBody = body.buffer instanceof SharedArrayBuffer;
2326
+ const sharedBuffer = isSharedBufferBody ? body.buffer : new SharedArrayBuffer(body.byteLength);
2327
+ if (!isSharedBufferBody) {
2328
+ new Uint8Array(sharedBuffer).set(body);
2329
+ }
2330
+ const startOffset = isSharedBufferBody ? body.byteOffset : 0;
2331
+ const buildDataSource = (checksumAlgorithm, checksumHeader) => ({
2332
+ type: "sharedBuffer",
2333
+ sharedBuffer,
2334
+ startOffset,
2335
+ partSize,
2336
+ totalSize: contentLength,
2337
+ checksumAlgorithm,
2338
+ checksumHeader,
2339
+ });
2340
+ return this.threadedMultipartUpload(request, contentLength, buildDataSource, transferOptions, emitEvents);
2341
+ }
2342
+ async getWorkerUploadMaxAttempts() {
2343
+ const maxAttemptsProvider = this.s3.config.maxAttempts;
2344
+ return maxAttemptsProvider();
2345
+ }
2346
+ isRetryableWorkerUploadError(error) {
2347
+ const sdkError = error;
2348
+ return isThrottlingError(sdkError) || isTransientError(sdkError);
2349
+ }
2350
+ async waitForWorkerUploadRetry(attempt) {
2351
+ const maxDelayMs = 1_000;
2352
+ const baseDelayMs = 100;
2353
+ const exponentialDelayMs = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
2354
+ const jitteredDelayMs = Math.round(exponentialDelayMs * (0.5 + Math.random()));
2355
+ await new Promise((resolve) => setTimeout(resolve, jitteredDelayMs));
2356
+ }
2357
+ async threadedMultipartUpload(request, contentLength, buildDataSource, transferOptions, emitEvents) {
2358
+ const { partSize, expectedPartsCount } = this.calculatePartSize(contentLength);
2359
+ this.checkAborted(transferOptions);
2360
+ const createMpuRequest = { ...request };
2361
+ const hasFullObjectChecksum = request.ContentMD5 ||
2362
+ request.ChecksumCRC32 ||
2363
+ request.ChecksumCRC32C ||
2364
+ request.ChecksumCRC64NVME ||
2365
+ request.ChecksumSHA1 ||
2366
+ request.ChecksumSHA256;
2367
+ if (hasFullObjectChecksum) {
2368
+ createMpuRequest.ChecksumType = "FULL_OBJECT";
2369
+ }
2370
+ if (!createMpuRequest.ChecksumAlgorithm && this.requestChecksumCalculation === "WHEN_SUPPORTED") {
2371
+ createMpuRequest.ChecksumAlgorithm = request.ChecksumAlgorithm ?? "CRC32";
2372
+ }
2373
+ const createMpuResponse = await this.s3.send(new CreateMultipartUploadCommand(createMpuRequest), transferOptions);
2374
+ const uploadId = createMpuResponse.UploadId;
2375
+ const checksumAlgorithm = createMpuRequest.ChecksumAlgorithm;
2376
+ const checksumHeader = checksumAlgorithm ? `x-amz-checksum-${checksumAlgorithm.toLowerCase()}` : undefined;
2377
+ const dataSource = buildDataSource(checksumAlgorithm, checksumHeader);
2378
+ const abortController = new AbortController();
2379
+ const uploadAbortSignal = abortController.signal;
2380
+ const maxWorkerUploadAttempts = await this.getWorkerUploadMaxAttempts();
2381
+ const sendUploadPart = async (partNumber, length) => {
2382
+ for (let attempt = 1;; attempt++) {
2383
+ if (uploadAbortSignal.aborted) {
2384
+ throw Object.assign(new Error("Upload aborted due to part failure."), { name: "AbortError" });
2385
+ }
2386
+ this.checkAborted(transferOptions);
2387
+ const partRequest = {
2388
+ ...request,
2389
+ Body: this.deps.createEmptyReadable(),
2390
+ UploadId: uploadId,
2391
+ PartNumber: partNumber,
2392
+ ContentLength: length,
2393
+ ...(checksumAlgorithm && {
2394
+ ChecksumAlgorithm: checksumAlgorithm,
2395
+ }),
2396
+ };
2397
+ try {
2398
+ return await this.s3.send(new UploadPartCommand(partRequest), {
2399
+ ...transferOptions,
2400
+ dataSource,
2401
+ });
2402
+ }
2403
+ catch (error) {
2404
+ if (attempt >= maxWorkerUploadAttempts || !this.isRetryableWorkerUploadError(error)) {
2405
+ throw error;
2406
+ }
2407
+ const errorName = error.name ?? "unknown error";
2408
+ this.logger.warn(`Retrying UploadPart ${partNumber} for ${request.Key} after ${errorName} ` +
2409
+ `(attempt ${attempt + 1}/${maxWorkerUploadAttempts}).`);
2410
+ await this.waitForWorkerUploadRetry(attempt);
2411
+ }
2412
+ }
2413
+ };
2414
+ try {
2415
+ const completedParts = [];
2416
+ let bytesUploaded = 0;
2417
+ let nextPartNumber = 1;
2418
+ const uploadPart = async () => {
2419
+ while (true) {
2420
+ const partNumber = nextPartNumber++;
2421
+ if (partNumber > expectedPartsCount)
2422
+ return;
2423
+ if (uploadAbortSignal.aborted) {
2424
+ throw Object.assign(new Error("Upload aborted due to part failure."), { name: "AbortError" });
2425
+ }
2426
+ this.checkAborted(transferOptions);
2427
+ const offset = (partNumber - 1) * partSize;
2428
+ const length = Math.min(partSize, contentLength - offset);
2429
+ const partResponse = await sendUploadPart(partNumber, length);
2430
+ const completedPart = {
2431
+ PartNumber: partNumber,
2432
+ ETag: partResponse.ETag,
2433
+ };
2434
+ if (partResponse.ChecksumCRC32)
2435
+ completedPart.ChecksumCRC32 = partResponse.ChecksumCRC32;
2436
+ if (partResponse.ChecksumCRC32C)
2437
+ completedPart.ChecksumCRC32C = partResponse.ChecksumCRC32C;
2438
+ if (partResponse.ChecksumCRC64NVME)
2439
+ completedPart.ChecksumCRC64NVME = partResponse.ChecksumCRC64NVME;
2440
+ if (partResponse.ChecksumSHA1)
2441
+ completedPart.ChecksumSHA1 = partResponse.ChecksumSHA1;
2442
+ if (partResponse.ChecksumSHA256)
2443
+ completedPart.ChecksumSHA256 = partResponse.ChecksumSHA256;
2444
+ completedParts.push(completedPart);
2445
+ bytesUploaded += length;
2446
+ if (emitEvents) {
2447
+ this.dispatchEvent(Object.assign(new Event("bytesTransferred"), {
2448
+ request,
2449
+ snapshot: {
2450
+ transferredBytes: bytesUploaded,
2451
+ totalBytes: contentLength,
2452
+ },
2453
+ }));
2454
+ }
2455
+ }
2456
+ };
2457
+ const partUploads = [];
2458
+ for (let i = 0; i < this.maxConcurrentUploads; i++) {
2459
+ partUploads.push(uploadPart().catch((error) => {
2460
+ abortController.abort();
2461
+ throw error;
2462
+ }));
2463
+ }
2464
+ await Promise.all(partUploads);
2465
+ if (completedParts.length !== expectedPartsCount) {
2466
+ throw new Error(`Expected ${expectedPartsCount} parts but uploaded ${completedParts.length} parts`);
2467
+ }
2468
+ completedParts.sort((a, b) => a.PartNumber - b.PartNumber);
2469
+ this.checkAborted(transferOptions);
2470
+ const { Body: _body, ...completeRequestFields } = request;
2471
+ const completeRequest = {
2472
+ ...completeRequestFields,
2473
+ UploadId: uploadId,
2474
+ MultipartUpload: { Parts: completedParts },
2475
+ MpuObjectSize: contentLength,
2476
+ };
2477
+ if (hasFullObjectChecksum) {
2478
+ completeRequest.ChecksumType = "FULL_OBJECT";
2479
+ if (request.ChecksumCRC32)
2480
+ completeRequest.ChecksumCRC32 = request.ChecksumCRC32;
2481
+ if (request.ChecksumCRC32C)
2482
+ completeRequest.ChecksumCRC32C = request.ChecksumCRC32C;
2483
+ if (request.ChecksumCRC64NVME)
2484
+ completeRequest.ChecksumCRC64NVME = request.ChecksumCRC64NVME;
2485
+ }
2486
+ try {
2487
+ return await this.s3.send(new CompleteMultipartUploadCommand(completeRequest), transferOptions);
2488
+ }
2489
+ catch (completeMpuError) {
2490
+ this.logger.warn(`CompleteMultipartUpload failed for upload ID ${uploadId}: ${completeMpuError.message}`);
2491
+ throw completeMpuError;
2492
+ }
2493
+ }
2494
+ catch (error) {
2495
+ const { Body: _abortBody, ...abortRequestFields } = request;
2496
+ const abortRequest = {
2497
+ ...abortRequestFields,
2498
+ UploadId: uploadId,
2499
+ };
2500
+ try {
2501
+ await this.s3.send(new AbortMultipartUploadCommand(abortRequest), transferOptions);
2502
+ }
2503
+ catch (abortError) {
2504
+ this.logger.warn(`Failed to abort multipart upload ${uploadId}:`, abortError);
2505
+ }
2506
+ throw error;
2507
+ }
2508
+ }
2509
+ async uploadInParts(request, contentLength, transferOptions, emitEvents) {
2510
+ const { partSize, expectedPartsCount } = this.calculatePartSize(contentLength);
2511
+ this.checkAborted(transferOptions);
2512
+ const createMpuRequest = { ...request };
2513
+ const hasFullObjectChecksum = request.ContentMD5 ||
2514
+ request.ChecksumCRC32 ||
2515
+ request.ChecksumCRC32C ||
2516
+ request.ChecksumCRC64NVME ||
2517
+ request.ChecksumSHA1 ||
2518
+ request.ChecksumSHA256;
2519
+ if (hasFullObjectChecksum) {
2520
+ createMpuRequest.ChecksumType = "FULL_OBJECT";
2521
+ }
2522
+ if (!createMpuRequest.ChecksumAlgorithm && this.requestChecksumCalculation === "WHEN_SUPPORTED") {
2523
+ createMpuRequest.ChecksumAlgorithm = request.ChecksumAlgorithm ?? "CRC32";
2524
+ }
2525
+ const createMpuResponse = await this.s3.send(new CreateMultipartUploadCommand(createMpuRequest), transferOptions);
2526
+ const uploadId = createMpuResponse.UploadId;
2527
+ const abortController = new AbortController();
2528
+ const uploadAbortSignal = abortController.signal;
2529
+ try {
2530
+ const completedParts = [];
2531
+ const dataFeeder = getChunk(request.Body, partSize);
2532
+ let bytesUploaded = 0;
2533
+ const uploadPart = async (dataFeeder) => {
2534
+ for await (const dataPart of dataFeeder) {
2535
+ if (uploadAbortSignal.aborted) {
2536
+ throw Object.assign(new Error("Upload aborted due to part failure."), { name: "AbortError" });
2537
+ }
2538
+ this.checkAborted(transferOptions);
2539
+ this.validateUploadPart(dataPart, partSize);
2540
+ const partRequest = {
2541
+ ...request,
2542
+ Body: dataPart.data,
2543
+ UploadId: uploadId,
2544
+ PartNumber: dataPart.partNumber,
2545
+ ContentLength: byteLength(dataPart.data),
2546
+ ...(this.requestChecksumCalculation === "WHEN_SUPPORTED" && {
2547
+ ChecksumAlgorithm: request.ChecksumAlgorithm,
2548
+ }),
2549
+ };
2550
+ const partResponse = await this.s3.send(new UploadPartCommand(partRequest), transferOptions);
2551
+ const checksumAlgorithm = partRequest.ChecksumAlgorithm;
2552
+ if (checksumAlgorithm &&
2553
+ !partResponse.ChecksumCRC32 &&
2554
+ !partResponse.ChecksumCRC32C &&
2555
+ !partResponse.ChecksumCRC64NVME &&
2556
+ !partResponse.ChecksumSHA1 &&
2557
+ !partResponse.ChecksumSHA256) {
2558
+ throw new Error(`Checksum was requested for part ${dataPart.partNumber} using ${checksumAlgorithm}, but no checksum was returned in the response. ` +
2559
+ `If running in a browser, ensure your S3 bucket CORS configuration is enabled. `);
2560
+ }
2561
+ const completedPart = {
2562
+ PartNumber: dataPart.partNumber,
2563
+ ETag: partResponse.ETag,
2564
+ };
2565
+ if (partResponse.ChecksumCRC32)
2566
+ completedPart.ChecksumCRC32 = partResponse.ChecksumCRC32;
2567
+ if (partResponse.ChecksumCRC32C)
2568
+ completedPart.ChecksumCRC32C = partResponse.ChecksumCRC32C;
2569
+ if (partResponse.ChecksumCRC64NVME)
2570
+ completedPart.ChecksumCRC64NVME = partResponse.ChecksumCRC64NVME;
2571
+ if (partResponse.ChecksumSHA1)
2572
+ completedPart.ChecksumSHA1 = partResponse.ChecksumSHA1;
2573
+ if (partResponse.ChecksumSHA256)
2574
+ completedPart.ChecksumSHA256 = partResponse.ChecksumSHA256;
2575
+ completedParts.push(completedPart);
2576
+ const partLength = byteLength(dataPart.data);
2577
+ bytesUploaded += partLength;
2578
+ if (emitEvents) {
2579
+ this.dispatchEvent(Object.assign(new Event("bytesTransferred"), {
2580
+ request,
2581
+ snapshot: {
2582
+ transferredBytes: bytesUploaded,
2583
+ totalBytes: contentLength,
2584
+ },
2585
+ }));
2586
+ }
2587
+ }
2588
+ };
2589
+ const partUploads = [];
2590
+ for (let i = 0; i < this.maxConcurrentUploads; i++) {
2591
+ partUploads.push(uploadPart(dataFeeder).catch((error) => {
2592
+ abortController.abort();
2593
+ throw error;
2594
+ }));
2595
+ }
2596
+ await Promise.all(partUploads);
2597
+ if (completedParts.length !== expectedPartsCount) {
2598
+ throw new Error(`Expected ${expectedPartsCount} parts but uploaded ${completedParts.length} parts`);
2599
+ }
2600
+ completedParts.sort((a, b) => a.PartNumber - b.PartNumber);
2601
+ this.checkAborted(transferOptions);
2602
+ const { Body: _body, ...completeRequestFields } = request;
2603
+ const completeRequest = {
2604
+ ...completeRequestFields,
2605
+ UploadId: uploadId,
2606
+ MultipartUpload: { Parts: completedParts },
2607
+ MpuObjectSize: contentLength,
2608
+ };
2609
+ if (hasFullObjectChecksum) {
2610
+ completeRequest.ChecksumType = "FULL_OBJECT";
2611
+ if (request.ChecksumCRC32)
2612
+ completeRequest.ChecksumCRC32 = request.ChecksumCRC32;
2613
+ if (request.ChecksumCRC32C)
2614
+ completeRequest.ChecksumCRC32C = request.ChecksumCRC32C;
2615
+ if (request.ChecksumCRC64NVME)
2616
+ completeRequest.ChecksumCRC64NVME = request.ChecksumCRC64NVME;
2617
+ }
2618
+ try {
2619
+ return await this.s3.send(new CompleteMultipartUploadCommand(completeRequest), transferOptions);
2620
+ }
2621
+ catch (completeMpuError) {
2622
+ this.logger.warn(`CompleteMultipartUpload failed for upload ID ${uploadId}: ${completeMpuError.message}`);
2623
+ throw completeMpuError;
2624
+ }
2625
+ }
2626
+ catch (error) {
2627
+ const { Body: _abortBody, ...abortRequestFields } = request;
2628
+ const abortRequest = {
2629
+ ...abortRequestFields,
2630
+ UploadId: uploadId,
2631
+ };
2632
+ try {
2633
+ await this.s3.send(new AbortMultipartUploadCommand(abortRequest), transferOptions);
2634
+ }
2635
+ catch (abortError) {
2636
+ this.logger.warn(`Failed to abort multipart upload ${uploadId}:`, abortError);
2637
+ }
2638
+ throw error;
2639
+ }
2640
+ }
2641
+ async *traverseDirectory(source, options) {
2642
+ const visited = new Set();
2643
+ const dir = await opendir(source, { recursive: options.recursive });
2644
+ for await (const entry of dir) {
2645
+ const fullPath = join(entry.parentPath ?? entry.path ?? source, entry.name);
2646
+ if (entry.isSymbolicLink()) {
2647
+ if (!options.followSymbolicLinks)
2648
+ continue;
2649
+ const realPath = await realpath(fullPath);
2650
+ const linkStat = await stat(realPath);
2651
+ if (linkStat.isDirectory()) {
2652
+ if (visited.has(realPath)) {
2653
+ throw new Error(`Circular symbolic link detected: ${fullPath} -> ${realPath}`);
2654
+ }
2655
+ visited.add(realPath);
2656
+ yield* this.traverseDirectory(realPath, options);
2657
+ continue;
2658
+ }
2659
+ if (!linkStat.isFile())
2660
+ continue;
2661
+ }
2662
+ else if (!entry.isFile()) {
2663
+ continue;
2664
+ }
2665
+ yield fullPath;
2666
+ }
2667
+ }
2668
+ deriveS3Key(source, filePath, s3Prefix) {
2669
+ let relativePath = relative(source, filePath);
2670
+ if (sep !== "/") {
2671
+ relativePath = relativePath.split(sep).join("/");
2672
+ }
2673
+ if (!s3Prefix)
2674
+ return relativePath;
2675
+ const normalizedPrefix = s3Prefix.endsWith("/") ? s3Prefix : s3Prefix + "/";
2676
+ return normalizedPrefix + relativePath;
2677
+ }
2678
+ validateUploadPart(dataPart, partSize) {
2679
+ const actualPartSize = byteLength(dataPart.data);
2680
+ if (actualPartSize === undefined) {
2681
+ throw new Error(`A dataPart was generated without a measurable data chunk size for part number ${dataPart.partNumber}`);
2682
+ }
2683
+ if (dataPart.partNumber === 1 && dataPart.lastPart) {
2684
+ return;
2685
+ }
2686
+ if (!dataPart.lastPart && actualPartSize !== partSize) {
2687
+ throw new Error(`The byte size for part number ${dataPart.partNumber}, size ${actualPartSize} does not match expected size ${partSize}`);
2688
+ }
2689
+ }
2690
+ }
2691
+ const bindS3TransferManager = (deps) => class S3TransferManager extends S3TransferManagerBase {
2692
+ constructor(config = {}) {
2693
+ super(config, deps);
2694
+ }
2695
+ };
2696
+ function matchesFilterRegExp(filter, value) {
2697
+ filter.lastIndex = 0;
2698
+ return filter.test(value);
2699
+ }
2700
+ function parseContentRangeStart(contentRange) {
2701
+ if (!contentRange)
2702
+ return undefined;
2703
+ const match = contentRange.match(/^bytes\s+(\d+)-/);
2704
+ return match ? Number(match[1]) : undefined;
2705
+ }
2706
+ const internalEventHandler = {
2707
+ async afterInitialGetObject() { },
2708
+ };
2709
+
2710
+ function buildDownloadError(msg) {
2711
+ const error = new Error(msg.error);
2712
+ if (msg.name)
2713
+ error.name = msg.name;
2714
+ if (msg.code)
2715
+ error.code = msg.code;
2716
+ return error;
2717
+ }
2718
+ function createEmptyReadable() {
2719
+ return new Readable({
2720
+ read() {
2721
+ this.push(null);
2722
+ },
2723
+ });
2724
+ }
2725
+ function defaultWorkerCount() {
2726
+ return cpus().length;
2727
+ }
2728
+ class WorkerHttpHandler {
2729
+ workers = [];
2730
+ inflightRequests = new Map();
2731
+ inflightDownloads = new Map();
2732
+ inflightStreamDownloads = new Map();
2733
+ nextRequestId = 0;
2734
+ workerInflightCounts = [];
2735
+ workerThreadCount;
2736
+ maxConcurrentUploads;
2737
+ maxConcurrentDownloads;
2738
+ useODirect;
2739
+ logger;
2740
+ initialized = false;
2741
+ initPromise;
2742
+ fallbackHandler;
2743
+ completedDownloads = new Map();
2744
+ completedTransfers = new Map();
2745
+ metadata = { handlerProtocol: "http/1.1" };
2746
+ constructor(options) {
2747
+ this.workerThreadCount = options?.workerThreadCount ?? defaultWorkerCount();
2748
+ this.maxConcurrentUploads = options?.maxConcurrentUploads ?? 32;
2749
+ this.maxConcurrentDownloads = options?.maxConcurrentDownloads ?? 32;
2750
+ this.useODirect = options?.useODirect ?? false;
2751
+ this.logger = options?.logger;
2752
+ this.fallbackHandler = new NodeHttpHandler();
2753
+ }
2754
+ getDownloadResult(token) {
2755
+ const result = this.completedDownloads.get(token);
2756
+ if (result) {
2757
+ this.completedDownloads.delete(token);
2758
+ }
2759
+ return result;
2760
+ }
2761
+ getStreamDownloadResult(token) {
2762
+ const result = this.completedTransfers.get(token);
2763
+ if (result) {
2764
+ this.completedTransfers.delete(token);
2765
+ }
2766
+ return result;
2767
+ }
2768
+ resolveWorkerConfig() {
2769
+ const submodulePath = path.join(__dirname, "..", "worker", "index.js");
2770
+ if (existsSync(submodulePath)) {
2771
+ return { workerPath: submodulePath };
2772
+ }
2773
+ const tsPath = path.join(__dirname, "..", "worker", "index.ts");
2774
+ if (existsSync(tsPath)) {
2775
+ return { workerPath: tsPath, workerOptions: { execArgv: ["--require", "tsx/cjs"] } };
2776
+ }
2777
+ return { workerPath: submodulePath };
2778
+ }
2779
+ ensureInitialized() {
2780
+ if (this.initialized)
2781
+ return Promise.resolve();
2782
+ if (this.initPromise)
2783
+ return this.initPromise;
2784
+ this.initPromise = new Promise((resolve, reject) => {
2785
+ const { workerPath, workerOptions } = this.resolveWorkerConfig();
2786
+ let readyCount = 0;
2787
+ let errorCount = 0;
2788
+ let settled = false;
2789
+ this.workerInflightCounts = new Array(this.workerThreadCount).fill(0);
2790
+ for (let i = 0; i < this.workerThreadCount; i++) {
2791
+ const worker = new Worker(workerPath, workerOptions);
2792
+ this.workers.push(worker);
2793
+ const workerIndex = i;
2794
+ worker.on("message", (msg) => {
2795
+ if (msg.type === "ready") {
2796
+ worker.postMessage({
2797
+ type: "config",
2798
+ maxSockets: Math.max(50, Math.ceil(Math.max(this.maxConcurrentUploads, this.maxConcurrentDownloads) / this.workerThreadCount)),
2799
+ useODirect: this.useODirect,
2800
+ });
2801
+ readyCount++;
2802
+ if (!settled && readyCount === this.workerThreadCount) {
2803
+ settled = true;
2804
+ this.initialized = true;
2805
+ resolve();
2806
+ }
2807
+ return;
2808
+ }
2809
+ if (msg.type === "workerLog") {
2810
+ this.logger?.[msg.level]?.(`[S3TransferManager:worker] ${msg.message}`);
2811
+ return;
2812
+ }
2813
+ if (msg.type === "httpResponse") {
2814
+ const pending = this.inflightRequests.get(msg.id);
2815
+ this.inflightRequests.delete(msg.id);
2816
+ if (pending) {
2817
+ this.workerInflightCounts[pending.workerIndex]--;
2818
+ pending.resolve({
2819
+ response: new HttpResponse({
2820
+ statusCode: msg.response.statusCode,
2821
+ headers: msg.response.headers,
2822
+ body: msg.response.body,
2823
+ }),
2824
+ });
2825
+ }
2826
+ return;
2827
+ }
2828
+ if (msg.type === "httpError") {
2829
+ const pending = this.inflightRequests.get(msg.id);
2830
+ this.inflightRequests.delete(msg.id);
2831
+ if (pending) {
2832
+ this.workerInflightCounts[pending.workerIndex]--;
2833
+ const error = Object.assign(new Error(msg.error), {
2834
+ ...(msg.code && { code: msg.code }),
2835
+ ...(msg.name && { name: msg.name }),
2836
+ });
2837
+ pending.reject(error);
2838
+ }
2839
+ return;
2840
+ }
2841
+ if (msg.type === "httpDownloadResult") {
2842
+ const pending = this.inflightDownloads.get(msg.id);
2843
+ this.inflightDownloads.delete(msg.id);
2844
+ if (pending) {
2845
+ this.workerInflightCounts[pending.workerIndex]--;
2846
+ pending.resolve({
2847
+ statusCode: msg.statusCode,
2848
+ headers: msg.headers,
2849
+ bytesWritten: msg.bytesWritten,
2850
+ checksum: msg.checksum,
2851
+ });
2852
+ }
2853
+ return;
2854
+ }
2855
+ if (msg.type === "httpDownloadError") {
2856
+ const pending = this.inflightDownloads.get(msg.id);
2857
+ this.inflightDownloads.delete(msg.id);
2858
+ if (pending) {
2859
+ this.workerInflightCounts[pending.workerIndex]--;
2860
+ pending.reject(buildDownloadError(msg));
2861
+ return;
2862
+ }
2863
+ const pendingTransfer = this.inflightStreamDownloads.get(msg.id);
2864
+ this.inflightStreamDownloads.delete(msg.id);
2865
+ if (pendingTransfer) {
2866
+ this.workerInflightCounts[pendingTransfer.workerIndex]--;
2867
+ pendingTransfer.reject(buildDownloadError(msg));
2868
+ }
2869
+ return;
2870
+ }
2871
+ if (msg.type === "httpDownloadHttpError") {
2872
+ const pending = this.inflightDownloads.get(msg.id);
2873
+ this.inflightDownloads.delete(msg.id);
2874
+ if (pending) {
2875
+ this.workerInflightCounts[pending.workerIndex]--;
2876
+ pending.resolve({
2877
+ statusCode: msg.statusCode,
2878
+ headers: msg.headers,
2879
+ bytesWritten: 0,
2880
+ errorBody: msg.body,
2881
+ });
2882
+ return;
2883
+ }
2884
+ const pendingTransfer = this.inflightStreamDownloads.get(msg.id);
2885
+ this.inflightStreamDownloads.delete(msg.id);
2886
+ if (pendingTransfer) {
2887
+ this.workerInflightCounts[pendingTransfer.workerIndex]--;
2888
+ pendingTransfer.resolve({
2889
+ rangeIndex: -1,
2890
+ buffer: new ArrayBuffer(0),
2891
+ byteLength: 0,
2892
+ statusCode: msg.statusCode,
2893
+ headers: msg.headers,
2894
+ errorBody: msg.body,
2895
+ });
2896
+ }
2897
+ return;
2898
+ }
2899
+ if (msg.type === "httpDownloadStreamResult") {
2900
+ const pending = this.inflightStreamDownloads.get(msg.id);
2901
+ this.inflightStreamDownloads.delete(msg.id);
2902
+ if (pending) {
2903
+ this.workerInflightCounts[pending.workerIndex]--;
2904
+ pending.resolve({
2905
+ rangeIndex: msg.rangeIndex,
2906
+ buffer: msg.buffer,
2907
+ byteLength: msg.byteLength,
2908
+ statusCode: msg.statusCode,
2909
+ headers: msg.headers,
2910
+ checksum: msg.checksum,
2911
+ });
2912
+ }
2913
+ return;
2914
+ }
2915
+ });
2916
+ worker.on("error", (err) => {
2917
+ if (!settled) {
2918
+ errorCount++;
2919
+ if (errorCount + readyCount === this.workerThreadCount) {
2920
+ settled = true;
2921
+ this.initPromise = undefined;
2922
+ reject(new Error(`Failed to initialize worker threads: ${err.message}`));
2923
+ }
2924
+ }
2925
+ for (const [id, pending] of this.inflightRequests) {
2926
+ if (pending.workerIndex === workerIndex) {
2927
+ pending.reject(err);
2928
+ this.inflightRequests.delete(id);
2929
+ }
2930
+ }
2931
+ for (const [id, pending] of this.inflightDownloads) {
2932
+ if (pending.workerIndex === workerIndex) {
2933
+ pending.reject(err);
2934
+ this.inflightDownloads.delete(id);
2935
+ }
2936
+ }
2937
+ for (const [id, pending] of this.inflightStreamDownloads) {
2938
+ if (pending.workerIndex === workerIndex) {
2939
+ pending.reject(err);
2940
+ this.inflightStreamDownloads.delete(id);
2941
+ }
2942
+ }
2943
+ this.workerInflightCounts[workerIndex] = Infinity;
2944
+ });
2945
+ }
2946
+ });
2947
+ return this.initPromise;
2948
+ }
2949
+ pickWorkerIndex() {
2950
+ let minIndex = 0;
2951
+ for (let i = 1; i < this.workerInflightCounts.length; i++) {
2952
+ if (this.workerInflightCounts[i] < this.workerInflightCounts[minIndex]) {
2953
+ minIndex = i;
2954
+ }
2955
+ }
2956
+ return minIndex;
2957
+ }
2958
+ dispatchToWorker(id, message) {
2959
+ const workerIndex = this.pickWorkerIndex();
2960
+ this.workerInflightCounts[workerIndex]++;
2961
+ return new Promise((resolve, reject) => {
2962
+ this.inflightRequests.set(id, { resolve, reject, workerIndex });
2963
+ this.workers[workerIndex].postMessage(message);
2964
+ });
2965
+ }
2966
+ dispatchDownloadToWorker(id, message) {
2967
+ const workerIndex = this.pickWorkerIndex();
2968
+ this.workerInflightCounts[workerIndex]++;
2969
+ return new Promise((resolve, reject) => {
2970
+ this.inflightDownloads.set(id, { resolve, reject, workerIndex });
2971
+ this.workers[workerIndex].postMessage(message);
2972
+ });
2973
+ }
2974
+ extractPartNumber(request) {
2975
+ const partNumber = request.query?.partNumber;
2976
+ if (partNumber) {
2977
+ return Number(Array.isArray(partNumber) ? partNumber[0] : partNumber);
2978
+ }
2979
+ return undefined;
2980
+ }
2981
+ returnBuffer(buffer, workerIndex) {
2982
+ const targetIndex = workerIndex ?? this.nextRequestId % this.workers.length;
2983
+ const message = { type: "returnBuffer", buffer };
2984
+ this.workers[targetIndex]?.postMessage(message, [buffer]);
2985
+ }
2986
+ async handle(request, options) {
2987
+ const downloadDataToFile = options?.downloadDataToFile;
2988
+ if (downloadDataToFile) {
2989
+ await this.ensureInitialized();
2990
+ const id = this.nextRequestId++;
2991
+ const serializedRequest = {
2992
+ method: request.method,
2993
+ protocol: request.protocol,
2994
+ hostname: request.hostname,
2995
+ port: request.port,
2996
+ path: request.path,
2997
+ query: request.query,
2998
+ headers: request.headers,
2999
+ };
3000
+ const message = {
3001
+ type: "httpDownloadToFile",
3002
+ id,
3003
+ request: serializedRequest,
3004
+ filePath: downloadDataToFile.filePath,
3005
+ offset: downloadDataToFile.offset,
3006
+ expectedLength: downloadDataToFile.expectedLength,
3007
+ checksumAlgorithm: downloadDataToFile.checksumAlgorithm,
3008
+ };
3009
+ const result = await this.dispatchDownloadToWorker(id, message);
3010
+ if (result.errorBody !== undefined) {
3011
+ return {
3012
+ response: new HttpResponse({
3013
+ statusCode: result.statusCode,
3014
+ headers: result.headers,
3015
+ body: Readable.from([Buffer.from(result.errorBody)]),
3016
+ }),
3017
+ };
3018
+ }
3019
+ if (downloadDataToFile.resultToken) {
3020
+ this.completedDownloads.set(downloadDataToFile.resultToken, {
3021
+ bytesWritten: result.bytesWritten,
3022
+ checksum: result.checksum,
3023
+ });
3024
+ }
3025
+ const responseHeaders = {};
3026
+ for (const [key, value] of Object.entries(result.headers)) {
3027
+ if (!key.startsWith("x-amz-checksum-")) {
3028
+ responseHeaders[key] = value;
3029
+ }
3030
+ }
3031
+ return {
3032
+ response: new HttpResponse({
3033
+ statusCode: result.statusCode,
3034
+ headers: responseHeaders,
3035
+ body: createEmptyReadable(),
3036
+ }),
3037
+ };
3038
+ }
3039
+ const downloadStream = options?.downloadStream;
3040
+ if (downloadStream) {
3041
+ await this.ensureInitialized();
3042
+ const id = this.nextRequestId++;
3043
+ const serializedRequest = {
3044
+ method: request.method,
3045
+ protocol: request.protocol,
3046
+ hostname: request.hostname,
3047
+ port: request.port,
3048
+ path: request.path,
3049
+ query: request.query,
3050
+ headers: request.headers,
3051
+ };
3052
+ const message = {
3053
+ type: "httpDownloadStream",
3054
+ id,
3055
+ request: serializedRequest,
3056
+ expectedSize: downloadStream.expectedSize,
3057
+ rangeIndex: downloadStream.rangeIndex,
3058
+ checksumAlgorithm: downloadStream.checksumAlgorithm,
3059
+ };
3060
+ const workerIndex = this.pickWorkerIndex();
3061
+ this.workerInflightCounts[workerIndex]++;
3062
+ const result = await new Promise((resolve, reject) => {
3063
+ this.inflightStreamDownloads.set(id, { resolve, reject, workerIndex });
3064
+ this.workers[workerIndex].postMessage(message);
3065
+ });
3066
+ if (result.errorBody !== undefined) {
3067
+ return {
3068
+ response: new HttpResponse({
3069
+ statusCode: result.statusCode,
3070
+ headers: result.headers,
3071
+ body: Readable.from([Buffer.from(result.errorBody)]),
3072
+ }),
3073
+ };
3074
+ }
3075
+ const responseHeaders = {};
3076
+ for (const [key, value] of Object.entries(result.headers)) {
3077
+ if (!key.startsWith("x-amz-checksum-")) {
3078
+ responseHeaders[key] = value;
3079
+ }
3080
+ }
3081
+ if (downloadStream.resultToken) {
3082
+ this.completedTransfers.set(downloadStream.resultToken, result);
3083
+ }
3084
+ return {
3085
+ response: new HttpResponse({
3086
+ statusCode: result.statusCode,
3087
+ headers: responseHeaders,
3088
+ body: createEmptyReadable(),
3089
+ }),
3090
+ };
3091
+ }
3092
+ const dataSource = options?.dataSource;
3093
+ const partNumber = this.extractPartNumber(request);
3094
+ if (!dataSource || !partNumber) {
3095
+ return this.fallbackHandler.handle(request, options);
3096
+ }
3097
+ await this.ensureInitialized();
3098
+ const id = this.nextRequestId++;
3099
+ const serializedRequest = {
3100
+ method: request.method,
3101
+ protocol: request.protocol,
3102
+ hostname: request.hostname,
3103
+ port: request.port,
3104
+ path: request.path,
3105
+ query: request.query,
3106
+ headers: request.headers,
3107
+ };
3108
+ if (dataSource.type === "file") {
3109
+ const { filePath, partSize, totalFileSize, startOffset = 0, checksumAlgorithm, checksumHeader } = dataSource;
3110
+ const partOffset = (partNumber - 1) * partSize;
3111
+ const length = Math.min(partSize, totalFileSize - partOffset);
3112
+ const offset = startOffset + partOffset;
3113
+ const message = {
3114
+ type: "httpRequestFromFile",
3115
+ id,
3116
+ request: serializedRequest,
3117
+ filePath,
3118
+ offset,
3119
+ length,
3120
+ checksumAlgorithm,
3121
+ checksumHeader,
3122
+ };
3123
+ return this.dispatchToWorker(id, message);
3124
+ }
3125
+ const { sharedBuffer, startOffset = 0, partSize, totalSize, checksumAlgorithm, checksumHeader } = dataSource;
3126
+ const partOffset = (partNumber - 1) * partSize;
3127
+ const offset = startOffset + partOffset;
3128
+ const length = Math.min(partSize, totalSize - partOffset);
3129
+ const message = {
3130
+ type: "httpRequestFromRAM",
3131
+ id,
3132
+ request: serializedRequest,
3133
+ sharedBuffer,
3134
+ offset,
3135
+ length,
3136
+ checksumAlgorithm,
3137
+ checksumHeader,
3138
+ };
3139
+ return this.dispatchToWorker(id, message);
3140
+ }
3141
+ updateHttpClientConfig(_key, _value) { }
3142
+ httpHandlerConfigs() {
3143
+ return {};
3144
+ }
3145
+ destroy() {
3146
+ for (const worker of this.workers) {
3147
+ worker.postMessage({ type: "done" });
3148
+ }
3149
+ this.workers = [];
3150
+ this.workerInflightCounts = [];
3151
+ this.initialized = false;
3152
+ this.initPromise = undefined;
3153
+ this.fallbackHandler.destroy();
3154
+ }
3155
+ }
3156
+
3157
+ class S3TransferManager extends bindS3TransferManager({
3158
+ joinStreams,
3159
+ destroyStreams,
3160
+ WorkerHttpHandler,
3161
+ createEmptyReadable,
3162
+ defaultWorkerCount,
3163
+ }) {
3164
+ }
3165
+
3166
+ exports.S3TransferManager = S3TransferManager;