Skip to content

Commit ba31070

Browse files
Move uploadId listing and adapt/add tests
Issue: CLDSRV-669
1 parent c2e8978 commit ba31070

File tree

4 files changed

+220
-590
lines changed

4 files changed

+220
-590
lines changed

lib/api/apiUtils/object/abortMultipartUpload.js

Lines changed: 19 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@ const async = require('async');
33
const constants = require('../../../../constants');
44
const { data } = require('../../../data/wrapper');
55
const locationConstraintCheck = require('../object/locationConstraintCheck');
6-
const { standardMetadataValidateBucketAndObj } =
7-
require('../../../metadata/metadataUtils');
6+
const metadataUtils = require('../../../metadata/metadataUtils');
87
const { validateQuotas } = require('../quotas/quotaUtils');
98
const services = require('../../../services');
109
const metadata = require('../../../metadata/wrapper');
11-
const { versioning } = require('arsenal');
1210

1311
function abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log,
1412
callback, request) {
@@ -31,7 +29,8 @@ function abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log,
3129

3230
async.waterfall([
3331
function checkDestBucketVal(next) {
34-
standardMetadataValidateBucketAndObj(metadataValParams, authzIdentityResult, log,
32+
// FIX: Call the function from the imported module object.
33+
metadataUtils.standardMetadataValidateBucketAndObj(metadataValParams, authzIdentityResult, log,
3534
(err, destinationBucket, objectMD) => {
3635
if (err) {
3736
log.error('error validating request', { error: err });
@@ -105,9 +104,13 @@ function abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log,
105104
// supported by Metadata.
106105
function ensureCleanupIsRequired(mpuBucket, storedParts, destBucket,
107106
objectMD, skipDataDelete, next) {
107+
if (!objectMD) {
108+
return next(null, mpuBucket, storedParts, destBucket, null, skipDataDelete);
109+
}
110+
108111
// If objectMD exists and has matching uploadId, use it directly
109112
// This handles all non-versioned cases, and some versioned cases.
110-
if (objectMD && objectMD.uploadId === uploadId) {
113+
if (objectMD.uploadId === uploadId) {
111114
return next(null, mpuBucket, storedParts, destBucket, objectMD, skipDataDelete);
112115
}
113116

@@ -117,64 +120,21 @@ function abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log,
117120
return next(null, mpuBucket, storedParts, destBucket, null, skipDataDelete);
118121
}
119122

120-
// Otherwise, we list all versions of the object. We try to stop as early
121-
// as possible, by using pagination.
122-
let keyMarker = null;
123-
let versionIdMarker = null;
124-
let foundVersion = null;
125-
let shouldContinue = true;
126-
127-
return async.whilst(
128-
() => shouldContinue && !foundVersion,
129-
callback => {
130-
const listParams = {
131-
listingType: 'DelimiterVersions',
132-
// To only list the specific key, we need to add the versionId separator
133-
prefix: `${objectKey}${versioning.VersioningConstants.VersionId.Separator}`,
134-
maxKeys: 1000,
135-
};
136-
137-
if (keyMarker) {
138-
listParams.keyMarker = keyMarker;
139-
}
140-
if (versionIdMarker) {
141-
listParams.versionIdMarker = versionIdMarker;
142-
}
143-
144-
return services.getObjectListing(bucketName, listParams, log, (err, listResponse) => {
145-
if (err) {
146-
log.error('error listing object versions', { error: err });
147-
return callback(err);
148-
}
149-
150-
// Check each version in current batch for matching uploadId
151-
const matchedVersion = (listResponse.Versions || []).find(version =>
152-
version.key === objectKey &&
153-
version.value &&
154-
version.value.uploadId === uploadId
155-
);
156-
157-
if (matchedVersion) {
158-
foundVersion = matchedVersion.value;
159-
}
160-
161-
// Set up for next iteration if needed
162-
if (listResponse.IsTruncated && !foundVersion) {
163-
keyMarker = listResponse.NextKeyMarker;
164-
versionIdMarker = listResponse.NextVersionIdMarker;
165-
} else {
166-
shouldContinue = false;
167-
}
168-
169-
return callback();
123+
// Otherwise, list all versions to find one with a matching uploadId.
124+
return services.findObjectVersionByUploadId(bucketName, objectKey, uploadId, log, (err, foundVersion) => {
125+
if (err) {
126+
log.error('error finding object version by uploadId, proceeding without cleanup', {
127+
error: err,
128+
method: 'abortMultipartUpload.ensureCleanupIsRequired',
170129
});
171-
},
172-
err => next(null, mpuBucket, storedParts, destBucket, err ? null : foundVersion, skipDataDelete)
173-
);
130+
// On error, continue the abort without an objectMD to clean up.
131+
return next(null, mpuBucket, storedParts, destBucket, null, skipDataDelete);
132+
}
133+
return next(null, mpuBucket, storedParts, destBucket, foundVersion, skipDataDelete);
134+
});
174135
},
175136
function deleteObjectMetadata(mpuBucket, storedParts, destBucket, objMDWithMatchingUploadID,
176137
skipDataDelete, next) {
177-
// If no objMDWithMatchingUploadID, nothing to delete
178138
if (!objMDWithMatchingUploadID) {
179139
return next(null, mpuBucket, storedParts, destBucket, objMDWithMatchingUploadID, skipDataDelete);
180140
}

lib/services.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const { errors, s3middleware } = require('arsenal');
66
const ObjectMD = require('arsenal').models.ObjectMD;
77
const BucketInfo = require('arsenal').models.BucketInfo;
88
const ObjectMDArchive = require('arsenal').models.ObjectMDArchive;
9+
const { versioning } = require('arsenal');
910
const acl = require('./metadata/acl');
1011
const constants = require('../constants');
1112
const { config } = require('./Config');
@@ -418,6 +419,71 @@ const services = {
418419
});
419420
},
420421

422+
/**
423+
* Finds a specific object version by its uploadId by listing and filtering all versions.
424+
* This is used during MPU abort to clean up potentially orphaned object metadata.
425+
* @param {string} bucketName - The name of the bucket.
426+
* @param {string} objectKey - The key of the object.
427+
* @param {string} uploadId - The uploadId to search for.
428+
* @param {object} log - The logger instance.
429+
* @param {function} cb - The callback, called with (err, foundVersion).
430+
* @returns {undefined}
431+
*/
432+
findObjectVersionByUploadId(bucketName, objectKey, uploadId, log, cb) {
433+
let keyMarker = null;
434+
let versionIdMarker = null;
435+
let foundVersion = null;
436+
let shouldContinue = true;
437+
438+
return async.whilst(
439+
() => shouldContinue && !foundVersion,
440+
callback => {
441+
const listParams = {
442+
listingType: 'DelimiterVersions',
443+
// To only list the specific key, we need to add the versionId separator
444+
prefix: `${objectKey}${versioning.VersioningConstants.VersionId.Separator}`,
445+
maxKeys: 1000,
446+
};
447+
448+
if (keyMarker) {
449+
listParams.keyMarker = keyMarker;
450+
}
451+
if (versionIdMarker) {
452+
listParams.versionIdMarker = versionIdMarker;
453+
}
454+
455+
return this.getObjectListing(bucketName, listParams, log, (err, listResponse) => {
456+
if (err) {
457+
log.error('error listing object versions', { error: err });
458+
return callback(err);
459+
}
460+
461+
// Check each version in current batch for matching uploadId
462+
const matchedVersion = (listResponse.Versions || []).find(version =>
463+
version.key === objectKey &&
464+
version.value &&
465+
version.value.uploadId === uploadId
466+
);
467+
468+
if (matchedVersion) {
469+
foundVersion = matchedVersion.value;
470+
}
471+
472+
// Set up for next iteration if needed
473+
if (listResponse.IsTruncated && !foundVersion) {
474+
keyMarker = listResponse.NextKeyMarker;
475+
versionIdMarker = listResponse.NextVersionIdMarker;
476+
} else {
477+
shouldContinue = false;
478+
}
479+
480+
return callback();
481+
});
482+
},
483+
err => cb(err, err ? null : foundVersion)
484+
);
485+
},
486+
421487
/**
422488
* Gets list of objects ready to be lifecycled
423489
* @param {object} bucketName - bucket in which objectMetadata is stored

0 commit comments

Comments
 (0)