Skip to content

Commit c4bf56a

Browse files
authored
Merge pull request #4 from lukachad/revised-sep-requirements
Revised spec requirements
2 parents 49fd801 + 1938251 commit c4bf56a

File tree

2 files changed

+86
-30
lines changed

2 files changed

+86
-30
lines changed

lib/lib-storage/src/s3-transfer-manager/S3TransferManager.e2e.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ describe(S3TransferManager.name, () => {
111111
}
112112
});
113113

114-
describe.skip("(SEP) download single object tests", () => {
114+
describe("(SEP) download single object tests", () => {
115115
async function sepTests(
116116
objectType: "single" | "multipart",
117117
multipartType: "PART" | "RANGE",

lib/lib-storage/src/s3-transfer-manager/S3TransferManager.ts

Lines changed: 85 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -211,29 +211,29 @@ export class S3TransferManager implements IS3TransferManager {
211211
/**
212212
* What is missing from the revised SEP and this implementation currently?
213213
* PART mode:
214-
* - Step 5: validate GetObject response for each part
215-
* - If validation fails at any point, cancel all ongoing requests and error out
214+
* - (DONE) Step 5: validate GetObject response for each part
215+
* - If validation fails at any point, cancel all ongoing requests and error out
216216
* - Step 6: after all requests have been sent, validate that the total number of part GET requests sent matches with the
217217
* expected `PartsCount`
218218
* - Step 7: when creating DownloadResponse, set accordingly:
219-
* - (DONE) `ContentLength` : total length of the object saved from Step 3
220-
* - (DONE) `ContentRange`: based on `bytes 0-(ContentLength -1)/ContentLength`
221-
* - If ChecksumType is `COMPOSITE`, set all checksum value members to null as
222-
* the checksum value returned from a part GET request is not the composite
223-
* checksum for the entire object
219+
* - (DONE) `ContentLength` : total length of the object saved from Step 3
220+
* - (DONE) `ContentRange`: based on `bytes 0-(ContentLength -1)/ContentLength`
221+
* - If ChecksumType is `COMPOSITE`, set all checksum value members to null as
222+
* the checksum value returned from a part GET request is not the composite
223+
* checksum for the entire object
224224
* RANGE mode:
225-
* - Step 7: validate GetObject response for each part. If validation fails or a
225+
* - (DONE) Step 7: validate GetObject response for each part. If validation fails or a
226226
* request fails at any point, cancel all ongoing requests and return an error to
227227
* the user.
228228
* - Step 8: after all requests have sent, validate that the total number of ranged
229229
* GET requests sent matches with the expected number saved from Step 5.
230230
* - Step 9: create DownloadResponse. Copy the fields in GetObject response from
231231
* Step 3 and set the following fields accordingly:
232-
* - (DONE) `ContentLength` : total length of the object saved from Step 3
233-
* - (DONE) `ContentRange`: based on `bytes 0-(ContentLength -1)/ContentLength`
234-
* - If ChecksumType is `COMPOSITE`, set all checksum value members to null as
235-
* the checksum value returned from a part GET request is not the composite
236-
* checksum for the entire object
232+
* - (DONE) `ContentLength` : total length of the object saved from Step 3
233+
* - (DONE) `ContentRange`: based on `bytes 0-(ContentLength -1)/ContentLength`
234+
* - If ChecksumType is `COMPOSITE`, set all checksum value members to null as
235+
* the checksum value returned from a part GET request is not the composite
236+
* checksum for the entire object
237237
* Checksum validation notes:
238238
* -
239239
*
@@ -370,8 +370,8 @@ export class S3TransferManager implements IS3TransferManager {
370370
};
371371
const initialPart = await this.s3ClientInstance.send(new GetObjectCommand(initialPartRequest), transferOptions);
372372
const initialETag = initialPart.ETag ?? undefined;
373+
const partSize = initialPart.ContentLength;
373374
totalSize = initialPart.ContentRange ? Number.parseInt(initialPart.ContentRange.split("/")[1]) : undefined;
374-
375375
this.dispatchTransferInitiatedEvent(request, totalSize);
376376
if (initialPart.Body) {
377377
if (initialPart.Body && typeof (initialPart.Body as any).getReader === "function") {
@@ -383,9 +383,12 @@ export class S3TransferManager implements IS3TransferManager {
383383
streams.push(Promise.resolve(initialPart.Body));
384384
requests.push(initialPartRequest);
385385
}
386+
386387
this.updateResponseLengthAndRange(initialPart, totalSize);
387388
this.assignMetadata(metadata, initialPart);
389+
this.updateChecksumValues(initialPart, metadata);
388390

391+
let partCount = 1;
389392
if (initialPart.PartsCount! > 1) {
390393
for (let part = 2; part <= initialPart.PartsCount!; part++) {
391394
this.checkAborted(transferOptions);
@@ -398,7 +401,7 @@ export class S3TransferManager implements IS3TransferManager {
398401
const getObject = this.s3ClientInstance
399402
.send(new GetObjectCommand(getObjectRequest), transferOptions)
400403
.then((response) => {
401-
// this.validatePartRange(part, response.ContentRange, this.targetPartSizeBytes);
404+
this.validatePartDownload(part, response.ContentRange, partSize ?? 0);
402405
if (response.Body && typeof (response.Body as any).getReader === "function") {
403406
const reader = (response.Body as any).getReader();
404407
(response.Body as any).getReader = function () {
@@ -410,8 +413,15 @@ export class S3TransferManager implements IS3TransferManager {
410413

411414
streams.push(getObject);
412415
requests.push(getObjectRequest);
416+
partCount++;
413417
}
414418
}
419+
420+
if (partCount !== initialPart.PartsCount) {
421+
throw new Error(
422+
`The number of parts downloaded (${partCount}) does not match the expected number (${initialPart.PartsCount})`
423+
);
424+
}
415425
} else {
416426
this.checkAborted(transferOptions);
417427

@@ -428,6 +438,7 @@ export class S3TransferManager implements IS3TransferManager {
428438
}
429439
this.updateResponseLengthAndRange(getObject, totalSize);
430440
this.assignMetadata(metadata, getObject);
441+
this.updateChecksumValues(getObject, metadata);
431442
}
432443

433444
return {
@@ -447,6 +458,7 @@ export class S3TransferManager implements IS3TransferManager {
447458
let left = 0;
448459
let right = this.targetPartSizeBytes - 1;
449460
let maxRange = Number.POSITIVE_INFINITY;
461+
let remainingLength = 1;
450462

451463
if (request.Range != null) {
452464
const [userRangeLeft, userRangeRight] = request.Range.replace("bytes=", "").split("-").map(Number);
@@ -455,17 +467,25 @@ export class S3TransferManager implements IS3TransferManager {
455467
left = userRangeLeft;
456468
right = Math.min(userRangeRight, left + S3TransferManager.MIN_PART_SIZE - 1);
457469
}
458-
459-
let remainingLength = 1;
460470
const getObjectRequest: GetObjectCommandInput = {
461471
...request,
462472
Range: `bytes=${left}-${right}`,
463473
};
464474
const initialRangeGet = await this.s3ClientInstance.send(new GetObjectCommand(getObjectRequest), transferOptions);
475+
this.validateRangeDownload(`bytes=${left}-${right}`, initialRangeGet.ContentRange);
465476
const initialETag = initialRangeGet.ETag ?? undefined;
466477
const totalSize = initialRangeGet.ContentRange
467478
? Number.parseInt(initialRangeGet.ContentRange.split("/")[1])
468479
: undefined;
480+
481+
let expectedRequestCount = 1;
482+
if (totalSize) {
483+
const contentLength = totalSize;
484+
const remainingBytes = Math.max(0, contentLength - (right - left + 1));
485+
const additionalRequests = Math.ceil(remainingBytes / S3TransferManager.MIN_PART_SIZE);
486+
expectedRequestCount += additionalRequests;
487+
}
488+
469489
if (initialRangeGet.Body && typeof (initialRangeGet.Body as any).getReader === "function") {
470490
const reader = (initialRangeGet.Body as any).getReader();
471491
(initialRangeGet.Body as any).getReader = function () {
@@ -474,16 +494,17 @@ export class S3TransferManager implements IS3TransferManager {
474494
}
475495

476496
this.dispatchTransferInitiatedEvent(request, totalSize);
477-
478497
streams.push(Promise.resolve(initialRangeGet.Body!));
479498
requests.push(getObjectRequest);
499+
480500
this.updateResponseLengthAndRange(initialRangeGet, totalSize);
481501
this.assignMetadata(metadata, initialRangeGet);
502+
this.updateChecksumValues(initialRangeGet, metadata);
482503

483504
left = right + 1;
484505
right = Math.min(left + S3TransferManager.MIN_PART_SIZE - 1, maxRange);
485506
remainingLength = totalSize ? Math.min(right - left + 1, Math.max(0, totalSize - left)) : 0;
486-
507+
let actualRequestCount = 1;
487508
while (remainingLength > 0) {
488509
this.checkAborted(transferOptions);
489510

@@ -496,6 +517,7 @@ export class S3TransferManager implements IS3TransferManager {
496517
const getObject = this.s3ClientInstance
497518
.send(new GetObjectCommand(getObjectRequest), transferOptions)
498519
.then((response) => {
520+
this.validateRangeDownload(range, response.ContentRange);
499521
if (response.Body && typeof (response.Body as any).getReader === "function") {
500522
const reader = (response.Body as any).getReader();
501523
(response.Body as any).getReader = function () {
@@ -507,12 +529,19 @@ export class S3TransferManager implements IS3TransferManager {
507529

508530
streams.push(getObject);
509531
requests.push(getObjectRequest);
532+
actualRequestCount++;
510533

511534
left = right + 1;
512535
right = Math.min(left + S3TransferManager.MIN_PART_SIZE - 1, maxRange);
513536
remainingLength = totalSize ? Math.min(right - left + 1, Math.max(0, totalSize - left)) : 0;
514537
}
515538

539+
if (expectedRequestCount !== actualRequestCount) {
540+
throw new Error(
541+
`The number of ranged GET requests sent (${actualRequestCount}) does not match the expected number (${expectedRequestCount})`
542+
);
543+
}
544+
516545
return {
517546
totalSize,
518547
};
@@ -541,6 +570,15 @@ export class S3TransferManager implements IS3TransferManager {
541570
}
542571
}
543572

573+
private updateChecksumValues(initialPart: DownloadResponse, metadata: Omit<DownloadResponse, "Body">) {
574+
if (initialPart.ChecksumType === "COMPOSITE") {
575+
metadata.ChecksumCRC32 = undefined;
576+
metadata.ChecksumCRC32C = undefined;
577+
metadata.ChecksumSHA1 = undefined;
578+
metadata.ChecksumSHA256 = undefined;
579+
}
580+
}
581+
544582
private checkAborted(transferOptions?: TransferOptions): void {
545583
if (transferOptions?.abortSignal?.aborted) {
546584
throw Object.assign(new Error("Download aborted."), { name: "AbortError" });
@@ -592,7 +630,7 @@ export class S3TransferManager implements IS3TransferManager {
592630
}
593631
}
594632

595-
private validatePartRange(partNumber: number, contentRange: string | undefined, partSize: number) {
633+
private validatePartDownload(partNumber: number, contentRange: string | undefined, partSize: number) {
596634
if (!contentRange) return;
597635

598636
const match = contentRange.match(/bytes (\d+)-(\d+)\/(\d+)/);
@@ -605,15 +643,6 @@ export class S3TransferManager implements IS3TransferManager {
605643
const expectedStart = (partNumber - 1) * partSize;
606644
const expectedEnd = Math.min(expectedStart + partSize - 1, total - 1);
607645

608-
// console.log({
609-
// partNumber,
610-
// start,
611-
// end,
612-
// total,
613-
// expectedStart,
614-
// expectedEnd
615-
// });
616-
617646
if (start !== expectedStart) {
618647
throw new Error(`Expected part ${partNumber} to start at ${expectedStart} but got ${start}`);
619648
}
@@ -622,4 +651,31 @@ export class S3TransferManager implements IS3TransferManager {
622651
throw new Error(`Expected part ${partNumber} to end at ${expectedEnd} but got ${end}`);
623652
}
624653
}
654+
655+
private validateRangeDownload(requestRange: string, responseRange: string | undefined) {
656+
if (!responseRange) return;
657+
658+
const match = responseRange.match(/bytes (\d+)-(\d+)\/(\d+)/);
659+
if (!match) throw new Error(`Invalid ContentRange format: ${responseRange}`);
660+
661+
const start = Number.parseInt(match[1]);
662+
const end = Number.parseInt(match[2]);
663+
const total = Number.parseInt(match[3]);
664+
665+
const rangeMatch = requestRange.match(/bytes=(\d+)-(\d+)/);
666+
if (!rangeMatch) throw new Error(`Invalid Range format: ${requestRange}`);
667+
668+
const expectedStart = Number.parseInt(rangeMatch[1]);
669+
const expectedEnd = Number.parseInt(rangeMatch[2]);
670+
671+
if (start !== expectedStart) {
672+
throw new Error(`Expected range to start at ${expectedStart} but got ${start}`);
673+
}
674+
675+
const isFinalPart = end + 1 === total;
676+
677+
if (end !== expectedEnd && !(isFinalPart && end < expectedEnd)) {
678+
throw new Error(`Expected range to end at ${expectedEnd} but got ${end}`);
679+
}
680+
}
625681
}

0 commit comments

Comments
 (0)