@@ -25,18 +25,20 @@ describe('Abort MPU', () => {
2525 bucketUtil = new BucketUtility ( 'default' , sigCfg ) ;
2626 s3 = bucketUtil . s3 ;
2727 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+ } ) ;
4042 } ) ;
4143
4244 afterEach ( ( ) =>
@@ -45,23 +47,24 @@ describe('Abort MPU', () => {
4547 Key : key ,
4648 UploadId : uploadId ,
4749 } ) . promise ( )
48- . then ( ( ) => bucketUtil . empty ( bucket ) )
49- . then ( ( ) => bucketUtil . deleteOne ( bucket ) )
50+ . then ( ( ) => bucketUtil . empty ( bucket ) )
51+ . then ( ( ) => bucketUtil . deleteOne ( bucket ) )
5052 ) ;
5153
5254 // aws-sdk now (v2.363.0) returns 'UriParameterError' error
5355 // this test was not replaced in any other suite
5456 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+ } ) ;
6367 } ) ;
64- } ) ;
6568 } ) ;
6669} ) ;
6770
@@ -273,16 +276,221 @@ describe('Abort MPU - No Such Upload', () => {
273276 afterEach ( ( ) => bucketUtil . deleteOne ( bucket ) ) ;
274277
275278 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+ } ) ;
285290 } ) ;
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 ) ;
286494 } ) ;
287495 } ) ;
288496} ) ;
0 commit comments