diff --git a/lib/core/shards/Shard.ts b/lib/core/shards/Shard.ts index 46f1d9f9..056b8487 100644 --- a/lib/core/shards/Shard.ts +++ b/lib/core/shards/Shard.ts @@ -25,7 +25,6 @@ export type ShardWithPossibleMultiUpload = Pick< 'hash' | 'uuid' > & { UploadId?: string; - parts?: { PartNumber: number; ETag: string }[]; }; export type ShardWithMultiUpload = Required; diff --git a/lib/server/routes/buckets.js b/lib/server/routes/buckets.js index 0611895d..e2c64e3a 100644 --- a/lib/server/routes/buckets.js +++ b/lib/server/routes/buckets.js @@ -1275,18 +1275,16 @@ BucketsRouter.prototype.finishUpload = async function (req, res, next) { return next(new errors.BadRequestError('Shards is not an array')); } - for (const { uuid, hash, UploadId, parts } of shards) { + for (const { uuid, hash, UploadId } of shards) { if (!uuidValidate(uuid)) { return next(new errors.BadRequestError('Invalid UUID')); } if (!hash) { return next(new errors.BadRequestError('Missing hash')); } - if (UploadId && !parts) { - return next(new errors.BadRequestError('For multipart: must provide also an array of parts for this upload')); - } - if (parts && !UploadId) { - return next(new errors.BadRequestError('For multipart: must provide also the UploadId for this upload')); + + if (UploadId !== undefined && (typeof UploadId !== 'string' || UploadId.length === 0)) { + return next(new errors.BadRequestError('Invalid UploadId')); } } diff --git a/tests/lib/e2e/buckets/files.e2e-spec.ts b/tests/lib/e2e/buckets/files.e2e-spec.ts index 5532ac12..733656fe 100644 --- a/tests/lib/e2e/buckets/files.e2e-spec.ts +++ b/tests/lib/e2e/buckets/files.e2e-spec.ts @@ -15,6 +15,7 @@ describe('Bridge E2E Tests', () => { let testUser: User let axiosGetStub: sinon.SinonStub + let axiosPostStub: sinon.SinonStub const databaseConnection = new StorageDbManager(); beforeAll(async () => { @@ -47,10 +48,12 @@ describe('Bridge E2E Tests', () => { if (url.includes('/v2/download/link')) return { data: { result: FAKE_DOWNLOAD_URL } } if (url.includes('exists')) return { status: 200 } }) + axiosPostStub = sinon.stub(axios, 'post').resolves() }) afterEach(() => { axiosGetStub.restore() + axiosPostStub.restore() }) describe('File Management v2', () => { @@ -221,7 +224,7 @@ describe('Bridge E2E Tests', () => { getAuth(testUser), bucketId, index, - (uploads as any[]).map((upload) => ({ hash: crypto.randomBytes(20).toString('hex'), uuid: upload.uuid })), + (uploads as any[]).map((upload) => ({ UploadId: crypto.randomBytes(20).toString('hex'), hash: crypto.randomBytes(20).toString('hex'), uuid: upload.uuid })), { shardSize: MB100 / 2 }, ) @@ -230,6 +233,9 @@ describe('Bridge E2E Tests', () => { const body = responseComplete.body; + expect( + axiosPostStub.calledWithMatch(sinon.match((url: string) => url.includes('/v2/upload-multipart-complete/link/'))) + ).toBe(true); expect(body).toMatchObject({ bucket: bucketId, created: expect.any(String), @@ -245,6 +251,67 @@ describe('Bridge E2E Tests', () => { }); + it('When a user finishes a multipart and provides an invalid uploadId, then it should fail', async () => { + const { body: { id: bucketId } } = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + .expect(201) + + const MB100 = 100 * 1024 * 1024 + const { body: { uploads } } = await testServer + .post(`/v2/buckets/${bucketId}/files/start?multiparts=2`) + .set('Authorization', getAuth(testUser)) + .send({ uploads: [{ index: 0, size: MB100 / 2 }, { index: 1, size: MB100 / 2 }] }) + + const index = crypto.randomBytes(32).toString('hex'); + + const response = await testServer + .post(`/v2/buckets/${bucketId}/files/finish`) + .set('Authorization', getAuth(testUser)) + .send({ + index, + shards: (uploads as any[]).map((upload) => ({ + UploadId: '', + hash: crypto.randomBytes(20).toString('hex'), + uuid: upload.uuid, + })), + }) + + expect(response.status).toBe(400); + }); + + it('When a user finishes to upload a file with multipart upload and uploadId is not provided, then upload is not completed and fails', async () => { + // Arrange: Create a bucket + const { body: { id: bucketId } } = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + + // Arrange: start the upload + const MB100 = 100 * 1024 * 1024 + const response = await testServer.post(`/v2/buckets/${bucketId}/files/start?multiparts=2`) + .set('Authorization', getAuth(testUser)) + .send({ uploads: [{ index: 0, size: MB100 / 2, }, { index: 1, size: MB100 / 2, }] }) + + const { uploads } = response.body; + + // Act: finish the upload without UploadId in shards + const index = crypto.randomBytes(32).toString('hex'); + const responseComplete = await finishUpload( + getAuth(testUser), + bucketId, + index, + (uploads as any[]).map((upload) => ({ hash: crypto.randomBytes(20).toString('hex'), uuid: upload.uuid })), + // Upload is not completed, so meta is not found in the farmer so we return null to simulate that scenario + { shardSize: MB100 / 2, farmerReportedSize: null }, + ) + + // Assert: complete multipart was not called on the farmer and upload failed + expect( + axiosPostStub.calledWithMatch(sinon.match((url: string) => url.includes('/v2/upload-multipart-complete/link/'))) + ).toBe(false); + expect(responseComplete.status).toBe(404); + }); + it('When an user finished to upload a file, then file size should be added to the used space', async () => { // Arrange: Create a user with some used space const originalUser = await createTestUser();