@@ -8,6 +8,7 @@ const { standardMetadataValidateBucketAndObj } =
88const { validateQuotas } = require ( '../quotas/quotaUtils' ) ;
99const services = require ( '../../../services' ) ;
1010const metadata = require ( '../../../metadata/wrapper' ) ;
11+ const { versioning } = require ( 'arsenal' ) ;
1112
1213function abortMultipartUpload ( authInfo , bucketName , objectKey , uploadId , log ,
1314 callback , request ) {
@@ -61,61 +62,146 @@ function abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log,
6162 } ) ;
6263 } ,
6364 function abortExternalMpu ( mpuBucket , mpuOverviewObj , destBucket , objectMD ,
64- next ) {
65+ next ) {
6566 const location = mpuOverviewObj . controllingLocationConstraint ;
6667 const originalIdentityAuthzResults = request . actionImplicitDenies ;
6768 // eslint-disable-next-line no-param-reassign
6869 delete request . actionImplicitDenies ;
6970 return data . abortMPU ( objectKey , uploadId , location , bucketName ,
70- request , destBucket , locationConstraintCheck , log ,
71- ( err , skipDataDelete ) => {
72- // eslint-disable-next-line no-param-reassign
73- request . actionImplicitDenies = originalIdentityAuthzResults ;
74- if ( err ) {
75- log . error ( 'error aborting MPU' , { error : err } ) ;
76- return next ( err , destBucket ) ;
77- }
78- // for Azure and GCP we do not need to delete data
79- // for all other backends, skipDataDelete will be set to false
80- return next ( null , mpuBucket , destBucket , objectMD , skipDataDelete ) ;
81- } ) ;
71+ request , destBucket , locationConstraintCheck , log ,
72+ ( err , skipDataDelete ) => {
73+ // eslint-disable-next-line no-param-reassign
74+ request . actionImplicitDenies = originalIdentityAuthzResults ;
75+ if ( err ) {
76+ log . error ( 'error aborting MPU' , { error : err } ) ;
77+ return next ( err , destBucket ) ;
78+ }
79+ // for Azure and GCP we do not need to delete data
80+ // for all other backends, skipDataDelete will be set to false
81+ return next ( null , mpuBucket , mpuOverviewObj , destBucket , objectMD , skipDataDelete ) ;
82+ } ) ;
8283 } ,
83- function getPartLocations ( mpuBucket , destBucket , objectMD , skipDataDelete ,
84- next ) {
84+ function getPartLocations ( mpuBucket , mpuOverviewObj , destBucket , objectMD , skipDataDelete ,
85+ next ) {
8586 services . getMPUparts ( mpuBucket . getName ( ) , uploadId , log ,
8687 ( err , result ) => {
8788 if ( err ) {
8889 log . error ( 'error getting parts' , { error : err } ) ;
8990 return next ( err , destBucket ) ;
9091 }
9192 const storedParts = result . Contents ;
92- return next ( null , mpuBucket , storedParts , destBucket , objectMD ,
93- skipDataDelete ) ;
93+ return next ( null , mpuBucket , mpuOverviewObj , storedParts , destBucket , objectMD ,
94+ skipDataDelete ) ;
9495 } ) ;
9596 } ,
96- function deleteObjectMetadata ( mpuBucket , storedParts , destBucket , objectMD , skipDataDelete , next ) {
97- if ( ! objectMD || metadataValMPUparams . uploadId !== objectMD . uploadId ) {
97+ // During Abort, we dynamically detect if the previous CompleteMPU call
98+ // created potential object metadata wrongly, e.g. by creating
99+ // an object version when some of the parts are missing.
100+ // By passing a null objectMD, we tell the subsequent steps
101+ // to skip the cleanup.
102+ // Another approach is possible, but not supported by all backends:
103+ // to honor the uploadId filter in standardMetadataValidateBucketAndObj
104+ // ensuring the objMD returned has the right uploadId. But this is not
105+ // supported by Metadata.
106+ function ensureCleanupIsRequired ( mpuBucket , mpuOverviewObj , storedParts , destBucket ,
107+ objectMD , skipDataDelete , next ) {
108+ // If no overview object, skip - this wasn't a failed completeMPU
109+ if ( ! mpuOverviewObj ) {
110+ return next ( null , mpuBucket , storedParts , destBucket , null , skipDataDelete ) ;
111+ }
112+
113+ // If objectMD exists and has matching uploadId, use it directly
114+ // This handles all non-versioned cases, and some versioned cases.
115+ if ( objectMD && objectMD . uploadId === uploadId ) {
98116 return next ( null , mpuBucket , storedParts , destBucket , objectMD , skipDataDelete ) ;
99117 }
100- // In case there has been an error during cleanup after a complete MPU
101- // (e.g. failure to delete MPU MD in shadow bucket),
102- // we need to ensure that the MPU metadata is deleted.
103- log . debug ( 'Object has existing metadata, deleting them' , {
118+
119+ // If bucket is not versioned, no need to check versions:
120+ // as the uploadId is not the same, we skip the cleanup.
121+ if ( ! destBucket . isVersioningEnabled ( ) ) {
122+ return next ( null , mpuBucket , storedParts , destBucket , null , skipDataDelete ) ;
123+ }
124+
125+ // Otherwise, we list all versions of the object. We try to stop as early
126+ // as possible, by using pagination.
127+ let keyMarker = null ;
128+ let versionIdMarker = null ;
129+ let foundVersion = null ;
130+ let shouldContinue = true ;
131+
132+ return async . whilst (
133+ ( ) => shouldContinue && ! foundVersion ,
134+ callback => {
135+ const listParams = {
136+ listingType : 'DelimiterVersions' ,
137+ // To only list the specific key, we need to add the versionId separator
138+ prefix : `${ objectKey } ${ versioning . VersioningConstants . VersionId . Separator } ` ,
139+ maxKeys : 1000 ,
140+ } ;
141+
142+ if ( keyMarker ) {
143+ listParams . keyMarker = keyMarker ;
144+ }
145+ if ( versionIdMarker ) {
146+ listParams . versionIdMarker = versionIdMarker ;
147+ }
148+
149+ return services . getObjectListing ( bucketName , listParams , log , ( err , listResponse ) => {
150+ if ( err ) {
151+ log . error ( 'error listing object versions' , { error : err } ) ;
152+ return callback ( err ) ;
153+ }
154+
155+ // Check each version in current batch for matching uploadId
156+ const matchedVersion = ( listResponse . Versions || [ ] ) . find ( version =>
157+ version . key === objectKey &&
158+ version . value &&
159+ version . value . uploadId === uploadId
160+ ) ;
161+
162+ if ( matchedVersion ) {
163+ foundVersion = matchedVersion . value ;
164+ }
165+
166+ // Set up for next iteration if needed
167+ if ( listResponse . IsTruncated && ! foundVersion ) {
168+ keyMarker = listResponse . NextKeyMarker ;
169+ versionIdMarker = listResponse . NextVersionIdMarker ;
170+ } else {
171+ shouldContinue = false ;
172+ }
173+
174+ return callback ( ) ;
175+ } ) ;
176+ } ,
177+ err => next ( null , mpuBucket , storedParts , destBucket , err ? null : foundVersion , skipDataDelete )
178+ ) ;
179+ } ,
180+ function deleteObjectMetadata ( mpuBucket , storedParts , destBucket , objMDWithMatchingUploadID ,
181+ skipDataDelete , next ) {
182+ // If no objMDWithMatchingUploadID, nothing to delete
183+ if ( ! objMDWithMatchingUploadID ) {
184+ return next ( null , mpuBucket , storedParts , destBucket , objMDWithMatchingUploadID , skipDataDelete ) ;
185+ }
186+
187+ log . debug ( 'Object has existing metadata with matching uploadId, deleting them' , {
104188 method : 'abortMultipartUpload' ,
105189 bucketName,
106190 objectKey,
107191 uploadId,
108- versionId : objectMD . versionId ,
192+ versionId : objMDWithMatchingUploadID . versionId ,
109193 } ) ;
110- return metadata . deleteObjectMD ( bucketName , objectKey , { versionId : objectMD . versionId } , log , err => {
194+ return metadata . deleteObjectMD ( bucketName , objectKey , {
195+ versionId : objMDWithMatchingUploadID . versionId ,
196+ } , log , err => {
111197 if ( err ) {
112198 log . error ( 'error deleting object metadata' , { error : err } ) ;
113199 }
114- return next ( err , mpuBucket , storedParts , destBucket , objectMD , skipDataDelete ) ;
200+ return next ( err , mpuBucket , storedParts , destBucket , objMDWithMatchingUploadID , skipDataDelete ) ;
115201 } ) ;
116202 } ,
117- function deleteData ( mpuBucket , storedParts , destBucket , objectMD ,
118- skipDataDelete , next ) {
203+ function deleteData ( mpuBucket , storedParts , destBucket , objMDWithMatchingUploadID ,
204+ skipDataDelete , next ) {
119205 if ( skipDataDelete ) {
120206 return next ( null , mpuBucket , storedParts , destBucket ) ;
121207 }
@@ -126,9 +212,11 @@ function abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log,
126212 return next ( null , mpuBucket , storedParts , destBucket ) ;
127213 }
128214
129- if ( objectMD && objectMD . location && objectMD . uploadId === metadataValMPUparams . uploadId ) {
215+ // Add object data locations if they exist and we can trust uploadId matches
216+ if ( objMDWithMatchingUploadID && objMDWithMatchingUploadID . location ) {
130217 const existingLocations = new Set ( locations . map ( loc => loc . key ) ) ;
131- const remainingObjectLocations = objectMD . location . filter ( loc => ! existingLocations . has ( loc . key ) ) ;
218+ const remainingObjectLocations = objMDWithMatchingUploadID .
219+ location . filter ( loc => ! existingLocations . has ( loc . key ) ) ;
132220 locations . push ( ...remainingObjectLocations ) ;
133221 }
134222
0 commit comments