From 737472069cc9255ba1d0a7230f5f86b089b2e891 Mon Sep 17 00:00:00 2001 From: linkgoron Date: Sat, 7 Aug 2021 03:19:31 +0300 Subject: [PATCH] feat(lib-storage): use PUT from small uploads (#2605) Use Put for uploads smaller than part size in lib-storage --- lib/lib-storage/src/Upload.spec.ts | 406 +++++++++++++++++- lib/lib-storage/src/Upload.ts | 74 +++- .../src/chunks/getChunkBuffer.spec.ts | 12 +- lib/lib-storage/src/chunks/getChunkBuffer.ts | 3 +- lib/lib-storage/src/chunks/getChunkStream.ts | 1 + .../src/chunks/getDataReadable.spec.ts | 6 + .../src/chunks/getDataReadableStream.spec.ts | 6 + 7 files changed, 484 insertions(+), 24 deletions(-) diff --git a/lib/lib-storage/src/Upload.spec.ts b/lib/lib-storage/src/Upload.spec.ts index e7407754043b..ba1a5195e2a5 100644 --- a/lib/lib-storage/src/Upload.spec.ts +++ b/lib/lib-storage/src/Upload.spec.ts @@ -2,7 +2,15 @@ const sendMock = jest.fn().mockImplementation((x) => x); const createMultipartMock = jest.fn().mockResolvedValue({ UploadId: "mockuploadId", }); -const uploadPartMock = jest.fn().mockResolvedValue({ +const uploadPartMock = jest + .fn() + .mockResolvedValueOnce({ + ETag: "mock-upload-Etag", + }) + .mockResolvedValueOnce({ + ETag: "mock-upload-Etag-2", + }); +const putObjectMock = jest.fn().mockResolvedValue({ ETag: "mockEtag", }); const completeMultipartMock = jest.fn().mockResolvedValue({ @@ -25,14 +33,26 @@ jest.mock("@aws-sdk/client-s3", () => ({ UploadPartCommand: uploadPartMock, CompleteMultipartUploadCommand: completeMultipartMock, PutObjectTaggingCommand: putObjectTaggingMock, + PutObjectCommand: putObjectMock, })); import { S3 } from "@aws-sdk/client-s3"; import { Upload, Progress } from "./index"; +import { Readable } from "stream"; + +const DEFAULT_PART_SIZE = 1024 * 1024 * 5; describe(Upload.name, () => { beforeEach(() => { jest.clearAllMocks(); + uploadPartMock + .mockReset() + .mockResolvedValueOnce({ + ETag: "mock-upload-Etag", + }) + .mockResolvedValueOnce({ + ETag: "mock-upload-Etag-2", + }); }); const params = { @@ -55,7 +75,66 @@ describe(Upload.name, () => { done(); }); - it("should call multipart upload parts correctly", async (done) => { + it("should upload using PUT when empty buffer", async (done) => { + const buffer = Buffer.from(""); + const actionParams = { ...params, Body: buffer }; + const upload = new Upload({ + params: actionParams, + client: new S3({}), + }); + + await upload.done(); + + expect(sendMock).toHaveBeenCalledTimes(1); + + expect(putObjectMock).toHaveBeenCalledTimes(1); + expect(putObjectMock).toHaveBeenCalledWith({ + ...params, + Body: Buffer.from(""), + }); + // create multipartMock is not called. + expect(createMultipartMock).toHaveBeenCalledTimes(0); + // upload parts is not called. + expect(uploadPartMock).toHaveBeenCalledTimes(0); + // complete multipart upload is not called. + expect(completeMultipartMock).toHaveBeenCalledTimes(0); + // no tags were passed. + expect(putObjectTaggingMock).toHaveBeenCalledTimes(0); + + done(); + }); + + it("should upload using PUT when empty stream", async (done) => { + const stream = new Readable({}); + stream.push(null); + const actionParams = { ...params, Body: stream }; + const upload = new Upload({ + params: actionParams, + client: new S3({}), + }); + + await upload.done(); + + expect(sendMock).toHaveBeenCalledTimes(1); + + expect(putObjectMock).toHaveBeenCalledTimes(1); + expect(putObjectMock).toHaveBeenCalledWith({ + ...params, + Body: Buffer.from(""), + }); + // create multipartMock is not called. + expect(createMultipartMock).toHaveBeenCalledTimes(0); + // upload parts is not called. + expect(uploadPartMock).toHaveBeenCalledTimes(0); + // complete multipart upload is not called. + expect(completeMultipartMock).toHaveBeenCalledTimes(0); + // no tags were passed. + expect(putObjectTaggingMock).toHaveBeenCalledTimes(0); + + done(); + }); + + it("should upload using PUT when parts are smaller than one part", async (done) => { const upload = new Upload({ params, client: new S3({}), @@ -63,43 +142,192 @@ describe(Upload.name, () => { await upload.done(); - expect(sendMock).toHaveBeenCalledTimes(3); + expect(sendMock).toHaveBeenCalledTimes(1); + + expect(putObjectMock).toHaveBeenCalledTimes(1); + expect(putObjectMock).toHaveBeenCalledWith({ + ...params, + Body: Buffer.from(params.Body), + }); + // create multipartMock is not called. + expect(createMultipartMock).toHaveBeenCalledTimes(0); + // upload parts is not called. + expect(uploadPartMock).toHaveBeenCalledTimes(0); + // complete multipart upload is not called. + expect(completeMultipartMock).toHaveBeenCalledTimes(0); + // no tags were passed. + expect(putObjectTaggingMock).toHaveBeenCalledTimes(0); + + done(); + }); + + it("should upload using PUT when parts are smaller than one part stream", async (done) => { + const streamBody = Readable.from( + (function* () { + yield params.Body; + })() + ); + const upload = new Upload({ + params: { ...params, Body: streamBody }, + client: new S3({}), + }); + + await upload.done(); + expect(sendMock).toHaveBeenCalledTimes(1); + + expect(putObjectMock).toHaveBeenCalledTimes(1); + expect(putObjectMock).toHaveBeenCalledWith({ + ...params, + Body: Buffer.from(params.Body), + }); + // create multipartMock is not called. + expect(createMultipartMock).toHaveBeenCalledTimes(0); + // upload parts is not called. + expect(uploadPartMock).toHaveBeenCalledTimes(0); + // complete multipart upload is not called. + expect(completeMultipartMock).toHaveBeenCalledTimes(0); + // no tags were passed. + expect(putObjectTaggingMock).toHaveBeenCalledTimes(0); + + done(); + }); + + it("should upload using multi-part when parts are larger than part size", async (done) => { + // create a string that's larger than 5MB. + const partSize = 1024 * 1024 * 5; + const largeBuffer = Buffer.from("#".repeat(partSize + 10)); + const firstBuffer = largeBuffer.subarray(0, partSize); + const secondBuffer = largeBuffer.subarray(partSize); + const actionParams = { ...params, Body: largeBuffer }; + const upload = new Upload({ + params: actionParams, + client: new S3({}), + }); + + await upload.done(); + + expect(sendMock).toHaveBeenCalledTimes(4); // create multipartMock is called correctly. expect(createMultipartMock).toHaveBeenCalledTimes(1); - expect(createMultipartMock).toHaveBeenCalledWith(params); + expect(createMultipartMock).toHaveBeenCalledWith({ + ...actionParams, + Body: undefined, + }); // upload parts is called correctly. - expect(uploadPartMock).toHaveBeenCalledTimes(1); - expect(uploadPartMock).toHaveBeenCalledWith({ - ...params, - Body: Buffer.from(params.Body), + expect(uploadPartMock).toHaveBeenCalledTimes(2); + expect(uploadPartMock).toHaveBeenNthCalledWith(1, { + ...actionParams, + Body: firstBuffer, PartNumber: 1, UploadId: "mockuploadId", }); + expect(uploadPartMock).toHaveBeenNthCalledWith(2, { + ...actionParams, + Body: secondBuffer, + PartNumber: 2, + UploadId: "mockuploadId", + }); + // complete multipart upload is called correctly. expect(completeMultipartMock).toHaveBeenCalledTimes(1); expect(completeMultipartMock).toHaveBeenLastCalledWith({ - ...params, + ...actionParams, + Body: undefined, UploadId: "mockuploadId", MultipartUpload: { Parts: [ { - ETag: "mockEtag", + ETag: "mock-upload-Etag", PartNumber: 1, }, + { + ETag: "mock-upload-Etag-2", + PartNumber: 2, + }, ], }, }); // no tags were passed. expect(putObjectTaggingMock).toHaveBeenCalledTimes(0); + // put was not called + expect(putObjectMock).toHaveBeenCalledTimes(0); + done(); + }); + + it("should upload using multi-part when parts are larger than part size stream", async (done) => { + // create a string that's larger than 5MB. + const largeBuffer = Buffer.from("#".repeat(DEFAULT_PART_SIZE + 10)); + const firstBuffer = largeBuffer.subarray(0, DEFAULT_PART_SIZE); + const secondBuffer = largeBuffer.subarray(DEFAULT_PART_SIZE); + const streamBody = Readable.from( + (function* () { + yield largeBuffer; + })() + ); + const actionParams = { ...params, Body: streamBody }; + const upload = new Upload({ + params: actionParams, + client: new S3({}), + }); + + await upload.done(); + + expect(sendMock).toHaveBeenCalledTimes(4); + // create multipartMock is called correctly. + expect(createMultipartMock).toHaveBeenCalledTimes(1); + expect(createMultipartMock).toHaveBeenCalledWith({ + ...actionParams, + Body: undefined, + }); + + // upload parts is called correctly. + expect(uploadPartMock).toHaveBeenCalledTimes(2); + expect(uploadPartMock).toHaveBeenNthCalledWith(1, { + ...actionParams, + Body: firstBuffer, + PartNumber: 1, + UploadId: "mockuploadId", + }); + + expect(uploadPartMock).toHaveBeenNthCalledWith(2, { + ...actionParams, + Body: secondBuffer, + PartNumber: 2, + UploadId: "mockuploadId", + }); + // complete multipart upload is called correctly. + expect(completeMultipartMock).toHaveBeenCalledTimes(1); + expect(completeMultipartMock).toHaveBeenLastCalledWith({ + ...actionParams, + Body: undefined, + UploadId: "mockuploadId", + MultipartUpload: { + Parts: [ + { + ETag: "mock-upload-Etag", + PartNumber: 1, + }, + { + ETag: "mock-upload-Etag-2", + PartNumber: 2, + }, + ], + }, + }); + + // no tags were passed. + expect(putObjectTaggingMock).toHaveBeenCalledTimes(0); + // put was not called + expect(putObjectMock).toHaveBeenCalledTimes(0); done(); }); - it("should add tags to the object if tags have been added", async (done) => { + it("should add tags to the object if tags have been added PUT", async (done) => { const tags = [ { Key: "k1", @@ -119,7 +347,7 @@ describe(Upload.name, () => { await upload.done(); - expect(sendMock).toHaveBeenCalledTimes(4); + expect(sendMock).toHaveBeenCalledTimes(2); // tags were passed. expect(putObjectTaggingMock).toHaveBeenCalledTimes(1); @@ -133,6 +361,42 @@ describe(Upload.name, () => { done(); }); + it("should add tags to the object if tags have been added multi-part", async (done) => { + const largeBuffer = Buffer.from("#".repeat(DEFAULT_PART_SIZE + 10)); + const actionParams = { ...params, Body: largeBuffer }; + const tags = [ + { + Key: "k1", + Value: "v1", + }, + { + Key: "k2", + Value: "v2", + }, + ]; + + const upload = new Upload({ + params: actionParams, + tags, + client: new S3({}), + }); + + await upload.done(); + + expect(sendMock).toHaveBeenCalledTimes(5); + + // tags were passed. + expect(putObjectTaggingMock).toHaveBeenCalledTimes(1); + expect(putObjectTaggingMock).toHaveBeenCalledWith({ + ...actionParams, + Tagging: { + TagSet: tags, + }, + }); + + done(); + }); + it("should validate partsize", async (done) => { try { const upload = new Upload({ @@ -184,4 +448,122 @@ describe(Upload.name, () => { }); await upload.done(); }); + + it("should provide progress updates multi-part buffer", async (done) => { + const partSize = 1024 * 1024 * 5; + const largeBuffer = Buffer.from("#".repeat(partSize + 10)); + const firstBuffer = largeBuffer.subarray(0, partSize); + const actionParams = { ...params, Body: largeBuffer }; + const upload = new Upload({ + params: actionParams, + client: new S3({}), + }); + + let received = []; + upload.on("httpUploadProgress", (progress: Progress) => { + received.push(progress); + }); + await upload.done(); + expect(received[0]).toEqual({ + Key: params.Key, + Bucket: params.Bucket, + loaded: firstBuffer.byteLength, + part: 1, + total: largeBuffer.byteLength, + }); + expect(received[1]).toEqual({ + Key: params.Key, + Bucket: params.Bucket, + loaded: largeBuffer.byteLength, + part: 2, + total: largeBuffer.byteLength, + }); + expect(received.length).toBe(2); + done(); + }); + + it("should provide progress updates multi-part stream", async (done) => { + const partSize = 1024 * 1024 * 5; + const largeBuffer = Buffer.from("#".repeat(partSize + 10)); + const streamBody = Readable.from( + (function* () { + yield largeBuffer; + })() + ); + const actionParams = { ...params, Body: streamBody }; + const upload = new Upload({ + params: actionParams, + client: new S3({}), + }); + + let received = []; + upload.on("httpUploadProgress", (progress: Progress) => { + received.push(progress); + }); + await upload.done(); + expect(received[0]).toEqual({ + Key: params.Key, + Bucket: params.Bucket, + loaded: partSize, + part: 1, + total: undefined, + }); + expect(received[1]).toEqual({ + Key: params.Key, + Bucket: params.Bucket, + loaded: partSize + 10, + part: 2, + total: undefined, + }); + expect(received.length).toBe(2); + done(); + }); + + it("should provide progress updates empty buffer", async (done) => { + const buffer = Buffer.from(""); + const actionParams = { ...params, Body: buffer }; + const upload = new Upload({ + params: actionParams, + client: new S3({}), + }); + + let received = []; + upload.on("httpUploadProgress", (progress: Progress) => { + received.push(progress); + }); + await upload.done(); + expect(received[0]).toEqual({ + Key: params.Key, + Bucket: params.Bucket, + loaded: 0, + part: 1, + total: 0, + }); + expect(received.length).toBe(1); + done(); + }); + + it("should provide progress updates empty stream", async (done) => { + const stream = Readable.from((function* () {})()); + const actionParams = { ...params, Body: stream }; + const upload = new Upload({ + params: actionParams, + client: new S3({}), + }); + + let received = []; + upload.on("httpUploadProgress", (progress: Progress) => { + received.push(progress); + }); + await upload.done(); + expect(received[0]).toEqual({ + Key: params.Key, + Bucket: params.Bucket, + loaded: 0, + part: 1, + total: 0, + }); + expect(received.length).toBe(1); + done(); + }); }); diff --git a/lib/lib-storage/src/Upload.ts b/lib/lib-storage/src/Upload.ts index d15b0d24d94c..19dfd29b0ebc 100644 --- a/lib/lib-storage/src/Upload.ts +++ b/lib/lib-storage/src/Upload.ts @@ -2,7 +2,10 @@ import { CompletedPart, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, + CreateMultipartUploadCommandOutput, + PutObjectCommand, PutObjectCommandInput, + PutObjectCommandOutput, PutObjectTaggingCommand, ServiceOutputTypes, Tag, @@ -17,6 +20,7 @@ import { AbortController, AbortSignal } from "@aws-sdk/abort-controller"; export interface RawDataPart { partNumber: number; data: BodyDataTypes; + lastPart?: boolean; } const MIN_PART_SIZE = 1024 * 1024 * 5; @@ -43,12 +47,15 @@ export class Upload extends EventEmitter { // used in the upload. private abortController: AbortController; private concurrentUploaders: Promise[] = []; + private createMultiPartPromise?: Promise; private uploadedParts: CompletedPart[] = []; private uploadId?: string; - uploadEvent?: string; + private isMultiPart: boolean = true; + private putResponse?: PutObjectCommandOutput; + constructor(options: Options) { super(); @@ -86,6 +93,30 @@ export class Upload extends EventEmitter { super.on(event, listener); } + async __uploadUsingPut(dataPart: RawDataPart) { + this.isMultiPart = false; + const params = { ...this.params, Body: dataPart.data }; + const putResult = await this.client.send(new PutObjectCommand(params)); + this.putResponse = putResult; + const totalSize = byteLength(dataPart.data); + this.__notifyProgress({ + loaded: totalSize, + total: totalSize, + part: 1, + Key: this.params.Key, + Bucket: this.params.Bucket, + }); + } + + async __createMultipartUpload() { + if (!this.createMultiPartPromise) { + const createCommandParams = { ...this.params, Body: undefined }; + this.createMultiPartPromise = this.client.send(new CreateMultipartUploadCommand(createCommandParams)); + } + const createMultipartUploadResult = await this.createMultiPartPromise; + this.uploadId = createMultipartUploadResult.UploadId; + } + async __doConcurrentUpload(dataFeeder: AsyncGenerator): Promise { for await (const dataPart of dataFeeder) { if (this.uploadedParts.length > this.MAX_PARTS) { @@ -95,6 +126,22 @@ export class Upload extends EventEmitter { } try { + if (this.abortController.signal.aborted) { + return; + } + + // Use put instead of multi-part for one chunk uploads. + if (dataPart.partNumber === 1 && dataPart.lastPart) { + return await this.__uploadUsingPut(dataPart); + } + + if (!this.uploadId) { + await this.__createMultipartUpload(); + if (this.abortController.signal.aborted) { + return; + } + } + const partResult = await this.client.send( new UploadPartCommand({ ...this.params, @@ -122,6 +169,10 @@ export class Upload extends EventEmitter { Bucket: this.params.Bucket, }); } catch (e) { + // Failed to create multi-part or put + if (!this.uploadId) { + throw e; + } // on leavePartsOnError throw an error so users can deal with it themselves, // otherwise swallow the error. if (this.leavePartsOnError) { @@ -132,9 +183,6 @@ export class Upload extends EventEmitter { } async __doMultipartUpload(): Promise { - const createMultipartUploadResult = await this.client.send(new CreateMultipartUploadCommand(this.params)); - this.uploadId = createMultipartUploadResult.UploadId; - // Set up data input chunks. const dataFeeder = getChunk(this.params.Body, this.partSize); @@ -150,16 +198,22 @@ export class Upload extends EventEmitter { throw Object.assign(new Error("Upload aborted."), { name: "AbortError" }); } - this.uploadedParts.sort((a, b) => a.PartNumber! - b.PartNumber!); - const completeMultipartUpload = await this.client.send( - new CompleteMultipartUploadCommand({ + let result; + if (this.isMultiPart) { + this.uploadedParts.sort((a, b) => a.PartNumber! - b.PartNumber!); + + const uploadCompleteParams = { ...this.params, + Body: undefined, UploadId: this.uploadId, MultipartUpload: { Parts: this.uploadedParts, }, - }) - ); + }; + result = await this.client.send(new CompleteMultipartUploadCommand(uploadCompleteParams)); + } else { + result = this.putResponse!; + } // Add tags to the object after it's completed the upload. if (this.tags.length) { @@ -173,7 +227,7 @@ export class Upload extends EventEmitter { ); } - return completeMultipartUpload; + return result; } __notifyProgress(progress: Progress) { diff --git a/lib/lib-storage/src/chunks/getChunkBuffer.spec.ts b/lib/lib-storage/src/chunks/getChunkBuffer.spec.ts index b98ab64fde6d..3bb5ee763097 100644 --- a/lib/lib-storage/src/chunks/getChunkBuffer.spec.ts +++ b/lib/lib-storage/src/chunks/getChunkBuffer.spec.ts @@ -12,13 +12,19 @@ describe.only(getChunkBuffer.name, () => { const chunker = getChunkBuffer(buffer, chunklength); let chunkNum = 0; + const expectedNumberOfChunks = totalLength / chunklength; for await (const chunk of chunker) { chunkNum += 1; expect(byteLength(chunk.data)).toEqual(chunklength); expect(chunk.partNumber).toEqual(chunkNum); + if (chunkNum < expectedNumberOfChunks) { + expect(chunk.lastPart).toBe(undefined); + } else { + expect(chunk.lastPart).toBe(true); + } } - expect(chunkNum).toEqual(totalLength / chunklength); + expect(chunkNum).toEqual(expectedNumberOfChunks); done(); }); @@ -35,8 +41,11 @@ describe.only(getChunkBuffer.name, () => { expect(chunks.length).toEqual(3); expect(byteLength(chunks[0].data)).toBe(chunklength); + expect(chunks[0].lastPart).toBe(undefined); expect(byteLength(chunks[1].data)).toBe(chunklength); + expect(chunks[1].lastPart).toBe(undefined); expect(byteLength(chunks[2].data)).toBe(totalLength % chunklength); + expect(chunks[2].lastPart).toBe(true); done(); }); @@ -53,6 +62,7 @@ describe.only(getChunkBuffer.name, () => { expect(chunks.length).toEqual(1); expect(byteLength(chunks[0].data)).toBe(totalLength % chunklength); + expect(chunks[0].lastPart).toBe(true); done(); }); }); diff --git a/lib/lib-storage/src/chunks/getChunkBuffer.ts b/lib/lib-storage/src/chunks/getChunkBuffer.ts index 35445175865e..e3553a69fb0a 100644 --- a/lib/lib-storage/src/chunks/getChunkBuffer.ts +++ b/lib/lib-storage/src/chunks/getChunkBuffer.ts @@ -5,7 +5,7 @@ export async function* getChunkBuffer(data: Buffer, partSize: number): AsyncGene let startByte = 0; let endByte = partSize; - while (endByte < data.length) { + while (endByte < data.byteLength) { yield { partNumber, data: data.slice(startByte, endByte), @@ -18,5 +18,6 @@ export async function* getChunkBuffer(data: Buffer, partSize: number): AsyncGene yield { partNumber, data: data.slice(startByte), + lastPart: true, }; } diff --git a/lib/lib-storage/src/chunks/getChunkStream.ts b/lib/lib-storage/src/chunks/getChunkStream.ts index 8861f2dba53e..26d452cba6fe 100644 --- a/lib/lib-storage/src/chunks/getChunkStream.ts +++ b/lib/lib-storage/src/chunks/getChunkStream.ts @@ -39,5 +39,6 @@ export async function* getChunkStream( yield { partNumber, data: Buffer.concat(currentBuffer.chunks), + lastPart: true, }; } diff --git a/lib/lib-storage/src/chunks/getDataReadable.spec.ts b/lib/lib-storage/src/chunks/getDataReadable.spec.ts index 1f35ccea9a9e..c64c3682c14c 100644 --- a/lib/lib-storage/src/chunks/getDataReadable.spec.ts +++ b/lib/lib-storage/src/chunks/getDataReadable.spec.ts @@ -37,12 +37,15 @@ describe(chunkFromReadable.name, () => { expect(chunks.length).toBe(3); expect(byteLength(chunks[0].data)).toEqual(20); expect(chunks[0].partNumber).toEqual(1); + expect(chunks[0].lastPart).toBe(undefined); expect(byteLength(chunks[1].data)).toEqual(20); expect(chunks[1].partNumber).toEqual(2); + expect(chunks[1].lastPart).toBe(undefined); expect(byteLength(chunks[2].data)).toEqual(18); expect(chunks[2].partNumber).toEqual(3); + expect(chunks[2].lastPart).toBe(true); done(); }); @@ -57,6 +60,7 @@ describe(chunkFromReadable.name, () => { expect(chunks.length).toBe(1); expect(byteLength(chunks[0].data)).toEqual(byteLength(fileStream)); expect(chunks[0].partNumber).toEqual(1); + expect(chunks[0].lastPart).toBe(true); done(); }); @@ -67,8 +71,10 @@ describe(chunkFromReadable.name, () => { expect(chunks.length).toEqual(11); for (let index = 0; index < 10; index++) { expect(byteLength(chunks[index].data)).toEqual(_6MB); + expect(chunks[index].lastPart).toBe(undefined); } expect(byteLength(chunks[10].data)).toEqual(_6MB / 2); + expect(chunks[10].lastPart).toBe(true); done(); }); }); diff --git a/lib/lib-storage/src/chunks/getDataReadableStream.spec.ts b/lib/lib-storage/src/chunks/getDataReadableStream.spec.ts index 30e76dfa0105..1668a49f671a 100644 --- a/lib/lib-storage/src/chunks/getDataReadableStream.spec.ts +++ b/lib/lib-storage/src/chunks/getDataReadableStream.spec.ts @@ -41,6 +41,7 @@ describe("chunkFromReadable.name", () => { expect(chunks.length).toBe(1); expect(byteLength(chunks[0].data)).toEqual(68); expect(chunks[0].partNumber).toEqual(1); + expect(chunks[0].lastPart).toBe(true); done(); }); @@ -51,12 +52,15 @@ describe("chunkFromReadable.name", () => { expect(chunks.length).toBe(3); expect(byteLength(chunks[0].data)).toEqual(20); expect(chunks[0].partNumber).toEqual(1); + expect(chunks[0].lastPart).toBe(undefined); expect(byteLength(chunks[1].data)).toEqual(20); expect(chunks[1].partNumber).toEqual(2); + expect(chunks[1].lastPart).toBe(undefined); expect(byteLength(chunks[2].data)).toEqual(18); expect(chunks[2].partNumber).toEqual(3); + expect(chunks[2].lastPart).toBe(true); done(); }); @@ -66,8 +70,10 @@ describe("chunkFromReadable.name", () => { expect(chunks.length).toEqual(11); for (let index = 0; index < 10; index++) { expect(byteLength(chunks[index].data)).toEqual(_6MB); + expect(chunks[index].lastPart).toBe(undefined); } expect(byteLength(chunks[10].data)).toEqual(_6MB / 2); + expect(chunks[10].lastPart).toBe(true); done(); }); });