@@ -25,18 +25,20 @@ describe('Abort MPU', () => {
25
25
bucketUtil = new BucketUtility ( 'default' , sigCfg ) ;
26
26
s3 = bucketUtil . s3 ;
27
27
return s3 . createBucket ( { Bucket : bucket } ) . promise ( )
28
- . then ( ( ) => s3 . createMultipartUpload ( {
29
- Bucket : bucket , Key : key } ) . promise ( ) )
30
- . then ( res => {
31
- uploadId = res . UploadId ;
32
- return s3 . uploadPart ( { Bucket : bucket , Key : key ,
33
- PartNumber : 1 , UploadId : uploadId , Body : bodyFirstPart ,
34
- } ) . promise ( ) ;
35
- } )
36
- . catch ( err => {
37
- process . stdout . write ( `Error in beforeEach: ${ err } \n` ) ;
38
- throw err ;
39
- } ) ;
28
+ . then ( ( ) => s3 . createMultipartUpload ( {
29
+ Bucket : bucket , Key : key
30
+ } ) . promise ( ) )
31
+ . then ( res => {
32
+ uploadId = res . UploadId ;
33
+ return s3 . uploadPart ( {
34
+ Bucket : bucket , Key : key ,
35
+ PartNumber : 1 , UploadId : uploadId , Body : bodyFirstPart ,
36
+ } ) . promise ( ) ;
37
+ } )
38
+ . catch ( err => {
39
+ process . stdout . write ( `Error in beforeEach: ${ err } \n` ) ;
40
+ throw err ;
41
+ } ) ;
40
42
} ) ;
41
43
42
44
afterEach ( ( ) =>
@@ -45,23 +47,24 @@ describe('Abort MPU', () => {
45
47
Key : key ,
46
48
UploadId : uploadId ,
47
49
} ) . promise ( )
48
- . then ( ( ) => bucketUtil . empty ( bucket ) )
49
- . then ( ( ) => bucketUtil . deleteOne ( bucket ) )
50
+ . then ( ( ) => bucketUtil . empty ( bucket ) )
51
+ . then ( ( ) => bucketUtil . deleteOne ( bucket ) )
50
52
) ;
51
53
52
54
// aws-sdk now (v2.363.0) returns 'UriParameterError' error
53
55
// this test was not replaced in any other suite
54
56
it . skip ( 'should return InvalidRequest error if aborting without key' ,
55
- done => {
56
- s3 . abortMultipartUpload ( {
57
- Bucket : bucket ,
58
- Key : '' ,
59
- UploadId : uploadId } ,
60
- err => {
61
- checkError ( err , 'InvalidRequest' , 'A key must be specified' ) ;
62
- done ( ) ;
57
+ done => {
58
+ s3 . abortMultipartUpload ( {
59
+ Bucket : bucket ,
60
+ Key : '' ,
61
+ UploadId : uploadId
62
+ } ,
63
+ err => {
64
+ checkError ( err , 'InvalidRequest' , 'A key must be specified' ) ;
65
+ done ( ) ;
66
+ } ) ;
63
67
} ) ;
64
- } ) ;
65
68
} ) ;
66
69
} ) ;
67
70
@@ -273,16 +276,221 @@ describe('Abort MPU - No Such Upload', () => {
273
276
afterEach ( ( ) => bucketUtil . deleteOne ( bucket ) ) ;
274
277
275
278
it ( 'should return NoSuchUpload error when aborting non-existent mpu' ,
276
- done => {
277
- s3 . abortMultipartUpload ( {
278
- Bucket : bucket ,
279
- Key : key ,
280
- UploadId : uuidv4 ( ) . replace ( / - / g, '' ) } ,
281
- err => {
282
- assert . notEqual ( err , null , 'Expected failure but got success' ) ;
283
- assert . strictEqual ( err . code , 'NoSuchUpload' ) ;
284
- done ( ) ;
279
+ done => {
280
+ s3 . abortMultipartUpload ( {
281
+ Bucket : bucket ,
282
+ Key : key ,
283
+ UploadId : uuidv4 ( ) . replace ( / - / g, '' )
284
+ } ,
285
+ err => {
286
+ assert . notEqual ( err , null , 'Expected failure but got success' ) ;
287
+ assert . strictEqual ( err . code , 'NoSuchUpload' ) ;
288
+ done ( ) ;
289
+ } ) ;
285
290
} ) ;
291
+ } ) ;
292
+ } ) ;
293
+
294
+ describe ( 'Abort MPU - Versioned Bucket Cleanup' , function testSuite ( ) {
295
+ this . timeout ( 120000 ) ;
296
+
297
+ withV4 ( sigCfg => {
298
+ let bucketUtil ;
299
+ let s3 ;
300
+ const bucketName = `abort-mpu-versioned-${ Date . now ( ) } ` ;
301
+ const objectKey = 'test-object-with-versions' ;
302
+
303
+ beforeEach ( done => {
304
+ bucketUtil = new BucketUtility ( 'default' , sigCfg ) ;
305
+ s3 = bucketUtil . s3 ;
306
+
307
+ async . series ( [
308
+ next => s3 . createBucket ( { Bucket : bucketName } , next ) ,
309
+ next => s3 . putBucketVersioning ( {
310
+ Bucket : bucketName ,
311
+ VersioningConfiguration : { Status : 'Enabled' } ,
312
+ } , next ) ,
313
+ ] , done ) ;
314
+ } ) ;
315
+
316
+ afterEach ( async ( ) => {
317
+ // Clean up all multipart uploads
318
+ const listMPUResponse = await s3 . listMultipartUploads ( { Bucket : bucketName } ) . promise ( ) ;
319
+ await Promise . all ( listMPUResponse . Uploads . map ( upload =>
320
+ s3 . abortMultipartUpload ( {
321
+ Bucket : bucketName ,
322
+ Key : upload . Key ,
323
+ UploadId : upload . UploadId ,
324
+ } ) . promise ( ) . catch ( err => {
325
+ if ( err . code !== 'NoSuchUpload' ) throw err ;
326
+ } )
327
+ ) ) ;
328
+
329
+ // Clean up all object versions
330
+ const listVersionsResponse = await s3 . listObjectVersions ( { Bucket : bucketName } ) . promise ( ) ;
331
+ await Promise . all ( [
332
+ ...listVersionsResponse . Versions . map ( version =>
333
+ s3 . deleteObject ( {
334
+ Bucket : bucketName ,
335
+ Key : version . Key ,
336
+ VersionId : version . VersionId ,
337
+ } ) . promise ( )
338
+ ) ,
339
+ ...listVersionsResponse . DeleteMarkers . map ( marker =>
340
+ s3 . deleteObject ( {
341
+ Bucket : bucketName ,
342
+ Key : marker . Key ,
343
+ VersionId : marker . VersionId ,
344
+ } ) . promise ( )
345
+ ) ,
346
+ ] ) ;
347
+
348
+ await bucketUtil . deleteOne ( bucketName ) ;
349
+ } ) ;
350
+
351
+ it ( 'should handle aborting MPU with many versions of same object' , done => {
352
+ const numberOfVersions = 5 ;
353
+ let currentVersion = 0 ;
354
+ let finalUploadId ;
355
+
356
+ // Create multiple versions of the same object
357
+ async . whilst (
358
+ ( ) => currentVersion < numberOfVersions ,
359
+ callback => {
360
+ currentVersion ++ ;
361
+ const data = Buffer . from ( `Version ${ currentVersion } data` ) ;
362
+
363
+ async . waterfall ( [
364
+ next => {
365
+ s3 . createMultipartUpload ( {
366
+ Bucket : bucketName ,
367
+ Key : objectKey ,
368
+ } , ( err , result ) => {
369
+ assert . ifError ( err ) ;
370
+ if ( currentVersion === numberOfVersions ) {
371
+ finalUploadId = result . UploadId ; // Save the last one for aborting
372
+ }
373
+ next ( null , result . UploadId ) ;
374
+ } ) ;
375
+ } ,
376
+ ( uploadId , next ) => {
377
+ s3 . uploadPart ( {
378
+ Bucket : bucketName ,
379
+ Key : objectKey ,
380
+ PartNumber : 1 ,
381
+ UploadId : uploadId ,
382
+ Body : data ,
383
+ } , ( err , result ) => {
384
+ assert . ifError ( err ) ;
385
+ next ( null , uploadId , result . ETag ) ;
386
+ } ) ;
387
+ } ,
388
+ ( uploadId , etag , next ) => {
389
+ if ( currentVersion === numberOfVersions ) {
390
+ // Don't complete the last one - we'll abort it
391
+ return next ( ) ;
392
+ }
393
+
394
+ return s3 . completeMultipartUpload ( {
395
+ Bucket : bucketName ,
396
+ Key : objectKey ,
397
+ UploadId : uploadId ,
398
+ MultipartUpload : {
399
+ Parts : [ { ETag : etag , PartNumber : 1 } ] ,
400
+ } ,
401
+ } , next ) ;
402
+ } ,
403
+ ] , callback ) ;
404
+ } ,
405
+ err => {
406
+ assert . ifError ( err ) ;
407
+
408
+ // Now abort the final MPU
409
+ s3 . abortMultipartUpload ( {
410
+ Bucket : bucketName ,
411
+ Key : objectKey ,
412
+ UploadId : finalUploadId ,
413
+ } , err => {
414
+ assert . ifError ( err ) ;
415
+
416
+ // Verify we still have the correct number of completed versions
417
+ s3 . listObjectVersions ( { Bucket : bucketName } , ( err , data ) => {
418
+ assert . ifError ( err ) ;
419
+
420
+ const objectVersions = data . Versions . filter ( v => v . Key === objectKey ) ;
421
+ assert . strictEqual ( objectVersions . length , numberOfVersions - 1 ,
422
+ `Expected ${ numberOfVersions - 1 } versions after abort, got ${ objectVersions . length } ` ) ;
423
+
424
+ done ( ) ;
425
+ } ) ;
426
+ } ) ;
427
+ }
428
+ ) ;
429
+ } ) ;
430
+
431
+ it ( 'should handle abort MPU when object has no versions' , done => {
432
+ let uploadId ;
433
+ const data = Buffer . from ( 'test data for single MPU abort' ) ;
434
+
435
+ async . waterfall ( [
436
+ // Create and upload part for MPU
437
+ next => {
438
+ s3 . createMultipartUpload ( {
439
+ Bucket : bucketName ,
440
+ Key : objectKey ,
441
+ } , ( err , result ) => {
442
+ assert . ifError ( err ) ;
443
+ uploadId = result . UploadId ;
444
+ next ( ) ;
445
+ } ) ;
446
+ } ,
447
+ next => {
448
+ s3 . uploadPart ( {
449
+ Bucket : bucketName ,
450
+ Key : objectKey ,
451
+ PartNumber : 1 ,
452
+ UploadId : uploadId ,
453
+ Body : data ,
454
+ } , err => {
455
+ assert . ifError ( err ) ;
456
+ next ( ) ;
457
+ } ) ;
458
+ } ,
459
+
460
+ // Abort the MPU
461
+ next => {
462
+ s3 . abortMultipartUpload ( {
463
+ Bucket : bucketName ,
464
+ Key : objectKey ,
465
+ UploadId : uploadId ,
466
+ } , err => {
467
+ assert . ifError ( err ) ;
468
+ next ( ) ;
469
+ } ) ;
470
+ } ,
471
+
472
+ // Verify no object exists
473
+ next => {
474
+ s3 . getObject ( { Bucket : bucketName , Key : objectKey } , err => {
475
+ assert . notEqual ( err , null , 'Expected NoSuchKey error' ) ;
476
+ assert . strictEqual ( err . code , 'NoSuchKey' ) ;
477
+ next ( ) ;
478
+ } ) ;
479
+ } ,
480
+
481
+ // Verify no versions exist
482
+ next => {
483
+ s3 . listObjectVersions ( { Bucket : bucketName } , ( err , data ) => {
484
+ assert . ifError ( err ) ;
485
+
486
+ const objectVersions = data . Versions . filter ( v => v . Key === objectKey ) ;
487
+ assert . strictEqual ( objectVersions . length , 0 ,
488
+ `Expected 0 versions after abort, got ${ objectVersions . length } ` ) ;
489
+
490
+ next ( ) ;
491
+ } ) ;
492
+ } ,
493
+ ] , done ) ;
286
494
} ) ;
287
495
} ) ;
288
496
} ) ;
0 commit comments