From 4c27883e226613cac9cd410c409ad7b30efff5f2 Mon Sep 17 00:00:00 2001 From: Dimitrios Vasilas Date: Wed, 2 Sep 2026 12:59:11 +0300 Subject: [PATCH 1/3] CLDSRV-986: Pass overhead fields when abortMPU cleans up after completeMPU The cleanup deletes the object or version an earlier CompleteMultipartUpload created; without the overhead fields the resulting oplog entry carries no owner id, which makes SUR hit an assert. --- lib/api/apiUtils/object/abortMultipartUpload.js | 1 + tests/unit/api/apiUtils/object/abortMultipartUpload.spec.js | 3 +++ 2 files changed, 4 insertions(+) diff --git a/lib/api/apiUtils/object/abortMultipartUpload.js b/lib/api/apiUtils/object/abortMultipartUpload.js index 914a5453ab..15be220ec0 100644 --- a/lib/api/apiUtils/object/abortMultipartUpload.js +++ b/lib/api/apiUtils/object/abortMultipartUpload.js @@ -167,6 +167,7 @@ function abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log, const options = preprocessingVersioningDelete( bucketName, destBucket, objectMD, objectMD.versionId, config.nullVersionCompatMode); options.replayId = uploadId; + options.overheadField = constants.overheadField; // This is a ghost metadata, without data loss, so skip oplog options.doesNotNeedOpogUpdate = true; diff --git a/tests/unit/api/apiUtils/object/abortMultipartUpload.spec.js b/tests/unit/api/apiUtils/object/abortMultipartUpload.spec.js index 43de69a276..e9c1965b9d 100644 --- a/tests/unit/api/apiUtils/object/abortMultipartUpload.spec.js +++ b/tests/unit/api/apiUtils/object/abortMultipartUpload.spec.js @@ -5,6 +5,7 @@ const { errors } = require('arsenal'); const async = require('async'); const crypto = require('crypto'); +const { overheadField } = require('../../../../../constants'); const abortMultipartUpload = require('../../../../../lib/api/apiUtils/object/abortMultipartUpload'); const { bucketPut } = require('../../../../../lib/api/bucketPut'); const initiateMultipartUpload = require('../../../../../lib/api/initiateMultipartUpload'); @@ -305,6 +306,8 @@ describe('abortMultipartUpload', () => { assert.ifError(err); sinon.assert.calledOnce(deleteObjectMDStub); assert.strictEqual(deleteObjectMDStub.getCall(0).args[2].versionId, 'orphan-vid'); + assert.deepStrictEqual( + deleteObjectMDStub.getCall(0).args[2].overheadField, overheadField); done(); }, { ...abortRequest, query: { uploadId: 'abort-id' } }); }); From 174cb5bd67f8da22eeb3ffa6504713e831ad44d5 Mon Sep 17 00:00:00 2001 From: Dimitrios Vasilas Date: Wed, 2 Sep 2026 16:59:00 +0300 Subject: [PATCH 2/3] CLDSRV-986: Run prettier --- .../apiUtils/object/abortMultipartUpload.js | 420 ++++++++++-------- .../object/abortMultipartUpload.spec.js | 331 +++++++++----- 2 files changed, 453 insertions(+), 298 deletions(-) diff --git a/lib/api/apiUtils/object/abortMultipartUpload.js b/lib/api/apiUtils/object/abortMultipartUpload.js index 15be220ec0..a947801507 100644 --- a/lib/api/apiUtils/object/abortMultipartUpload.js +++ b/lib/api/apiUtils/object/abortMultipartUpload.js @@ -10,8 +10,7 @@ const metadata = require('../../../metadata/wrapper'); const { preprocessingVersioningDelete } = require('./versioning'); const { config } = require('../../../Config'); -function abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log, - callback, request) { +function abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log, callback, request) { const metadataValMPUparams = { authInfo, bucketName, @@ -29,230 +28,267 @@ function abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log, metadataValParams.requestType = 'objectPut'; const authzIdentityResult = request ? request.actionImplicitDenies : false; - async.waterfall([ - function checkDestBucketVal(next) { - metadataUtils.standardMetadataValidateBucketAndObj(metadataValParams, authzIdentityResult, log, - (err, destinationBucket, objectMD) => { - if (err) { - log.error('error validating request', { error: err }); - return next(err, destinationBucket); - } - if (destinationBucket.policies) { - // TODO: Check bucket policies to see if user is granted - // permission or forbidden permission to take - // given action. - // If permitted, add 'bucketPolicyGoAhead' - // attribute to params for validating at MPU level. - // This is GH Issue#76 - metadataValMPUparams.requestType = - 'bucketPolicyGoAhead'; - } - return next(null, destinationBucket, objectMD); - }); - }, - function checkMPUval(destBucket, objectMD, next) { - metadataValParams.log = log; - services.metadataValidateMultipart(metadataValParams, - (err, mpuBucket, mpuOverviewObj) => { + async.waterfall( + [ + function checkDestBucketVal(next) { + metadataUtils.standardMetadataValidateBucketAndObj( + metadataValParams, + authzIdentityResult, + log, + (err, destinationBucket, objectMD) => { + if (err) { + log.error('error validating request', { error: err }); + return next(err, destinationBucket); + } + if (destinationBucket.policies) { + // TODO: Check bucket policies to see if user is granted + // permission or forbidden permission to take + // given action. + // If permitted, add 'bucketPolicyGoAhead' + // attribute to params for validating at MPU level. + // This is GH Issue#76 + metadataValMPUparams.requestType = 'bucketPolicyGoAhead'; + } + return next(null, destinationBucket, objectMD); + }, + ); + }, + function checkMPUval(destBucket, objectMD, next) { + metadataValParams.log = log; + services.metadataValidateMultipart(metadataValParams, (err, mpuBucket, mpuOverviewObj) => { if (err) { log.error('error validating multipart', { error: err }); return next(err, destBucket); } return next(err, mpuBucket, mpuOverviewObj, destBucket, objectMD); }); - }, - function abortExternalMpu(mpuBucket, mpuOverviewObj, destBucket, objectMD, - next) { - const location = mpuOverviewObj.controllingLocationConstraint; - const originalIdentityAuthzResults = request.actionImplicitDenies; - // eslint-disable-next-line no-param-reassign - delete request.actionImplicitDenies; - return data.abortMPU(objectKey, uploadId, location, bucketName, - request, destBucket, locationConstraintCheck, log, - (err, skipDataDelete) => { + }, + function abortExternalMpu(mpuBucket, mpuOverviewObj, destBucket, objectMD, next) { + const location = mpuOverviewObj.controllingLocationConstraint; + const originalIdentityAuthzResults = request.actionImplicitDenies; // eslint-disable-next-line no-param-reassign - request.actionImplicitDenies = originalIdentityAuthzResults; - if (err) { - log.error('error aborting MPU', { error: err }); - return next(err, destBucket); - } - // for Azure and GCP we do not need to delete data - // for all other backends, skipDataDelete will be set to false - return next(null, mpuBucket, destBucket, objectMD, skipDataDelete); - }); - }, - function getPartLocations(mpuBucket, destBucket, objectMD, skipDataDelete, - next) { - services.getMPUparts(mpuBucket.getName(), uploadId, log, - (err, result) => { + delete request.actionImplicitDenies; + return data.abortMPU( + objectKey, + uploadId, + location, + bucketName, + request, + destBucket, + locationConstraintCheck, + log, + (err, skipDataDelete) => { + // eslint-disable-next-line no-param-reassign + request.actionImplicitDenies = originalIdentityAuthzResults; + if (err) { + log.error('error aborting MPU', { error: err }); + return next(err, destBucket); + } + // for Azure and GCP we do not need to delete data + // for all other backends, skipDataDelete will be set to false + return next(null, mpuBucket, destBucket, objectMD, skipDataDelete); + }, + ); + }, + function getPartLocations(mpuBucket, destBucket, objectMD, skipDataDelete, next) { + services.getMPUparts(mpuBucket.getName(), uploadId, log, (err, result) => { if (err) { log.error('error getting parts', { error: err }); return next(err, destBucket); } const storedParts = result.Contents; - return next(null, mpuBucket, storedParts, destBucket, objectMD, - skipDataDelete); + return next(null, mpuBucket, storedParts, destBucket, objectMD, skipDataDelete); }); - }, - // During Abort, we dynamically detect if the previous CompleteMPU call - // created potential object metadata wrongly, e.g. by creating - // an object version when some of the parts are missing. - // By passing a null objectMD, we tell the subsequent steps - // to skip the cleanup. - // Another approach is possible, but not supported by all backends: - // to honor the uploadId filter in standardMetadataValidateBucketAndObj - // ensuring the objMD returned has the right uploadId. But this is not - // supported by Metadata. - function findObjectToCleanup(mpuBucket, storedParts, destBucket, - objectMD, skipDataDelete, next) { - if (!objectMD) { - return next(null, mpuBucket, storedParts, destBucket, null, skipDataDelete); - } - - // If objectMD exists and has matching uploadId, use it directly - // This handles all non-versioned cases, and some versioned cases. - if (objectMD.uploadId === uploadId) { - return next(null, mpuBucket, storedParts, destBucket, objectMD, skipDataDelete); - } - - // If bucket is not versioned, no need to check versions: - // as the uploadId is not the same, we skip the cleanup. - if (!destBucket.isVersioningEnabled()) { - return next(null, mpuBucket, storedParts, destBucket, null, skipDataDelete); - } - - // Otherwise, list all versions to find one with a matching uploadId. - return services.findObjectVersionByUploadId(bucketName, objectKey, uploadId, log, (err, foundVersion) => { - if (err) { - log.warn('error finding object version by uploadId, proceeding without cleanup', { - error: err, - method: 'abortMultipartUpload.findObjectToCleanup', - }); - // On error, continue the abort without an objectMD to clean up. + }, + // During Abort, we dynamically detect if the previous CompleteMPU call + // created potential object metadata wrongly, e.g. by creating + // an object version when some of the parts are missing. + // By passing a null objectMD, we tell the subsequent steps + // to skip the cleanup. + // Another approach is possible, but not supported by all backends: + // to honor the uploadId filter in standardMetadataValidateBucketAndObj + // ensuring the objMD returned has the right uploadId. But this is not + // supported by Metadata. + function findObjectToCleanup(mpuBucket, storedParts, destBucket, objectMD, skipDataDelete, next) { + if (!objectMD) { return next(null, mpuBucket, storedParts, destBucket, null, skipDataDelete); } - if (!foundVersion) { + + // If objectMD exists and has matching uploadId, use it directly + // This handles all non-versioned cases, and some versioned cases. + if (objectMD.uploadId === uploadId) { + return next(null, mpuBucket, storedParts, destBucket, objectMD, skipDataDelete); + } + + // If bucket is not versioned, no need to check versions: + // as the uploadId is not the same, we skip the cleanup. + if (!destBucket.isVersioningEnabled()) { return next(null, mpuBucket, storedParts, destBucket, null, skipDataDelete); } - // foundVersion is not in objectMD format, see MetadataWrapper _parseListEntries - // multiple fields are missing or differ in list entry versus objectMD - return metadataUtils.metadataGetObject(bucketName, objectKey, foundVersion.VersionId, null, log, - (err, objectMD) => { + + // Otherwise, list all versions to find one with a matching uploadId. + return services.findObjectVersionByUploadId( + bucketName, + objectKey, + uploadId, + log, + (err, foundVersion) => { if (err) { - log.warn('error getting object version metadata, proceeding without cleanup', { + log.warn('error finding object version by uploadId, proceeding without cleanup', { error: err, method: 'abortMultipartUpload.findObjectToCleanup', }); // On error, continue the abort without an objectMD to clean up. return next(null, mpuBucket, storedParts, destBucket, null, skipDataDelete); } - return next(null, mpuBucket, storedParts, destBucket, objectMD, skipDataDelete); - }); - }); - }, - function deleteObjectMetadata(mpuBucket, storedParts, destBucket, objectMD, - skipDataDelete, next) { - if (!objectMD) { - return next(null, mpuBucket, storedParts, destBucket, objectMD, skipDataDelete); - } - - log.debug('Object has existing metadata, deleting them', { - method: 'abortMultipartUpload', - bucketName, - objectKey, - uploadId, - versionId: objectMD.versionId, - }); - - const options = preprocessingVersioningDelete( - bucketName, destBucket, objectMD, objectMD.versionId, config.nullVersionCompatMode); - options.replayId = uploadId; - options.overheadField = constants.overheadField; - // This is a ghost metadata, without data loss, so skip oplog - options.doesNotNeedOpogUpdate = true; - - return metadata.deleteObjectMD(bucketName, objectKey, options, log, err => { - if (err) { - // Handle concurrent deletion of this object metadata - if (err.is?.NoSuchKey) { - log.debug('object metadata already deleted or does not exist', { - method: 'abortMultipartUpload', + if (!foundVersion) { + return next(null, mpuBucket, storedParts, destBucket, null, skipDataDelete); + } + // foundVersion is not in objectMD format, see MetadataWrapper _parseListEntries + // multiple fields are missing or differ in list entry versus objectMD + return metadataUtils.metadataGetObject( bucketName, objectKey, - versionId: objectMD.versionId, - }); - } else { - log.error('error deleting object metadata', { error: err }); - } + foundVersion.VersionId, + null, + log, + (err, objectMD) => { + if (err) { + log.warn('error getting object version metadata, proceeding without cleanup', { + error: err, + method: 'abortMultipartUpload.findObjectToCleanup', + }); + // On error, continue the abort without an objectMD to clean up. + return next(null, mpuBucket, storedParts, destBucket, null, skipDataDelete); + } + return next(null, mpuBucket, storedParts, destBucket, objectMD, skipDataDelete); + }, + ); + }, + ); + }, + function deleteObjectMetadata(mpuBucket, storedParts, destBucket, objectMD, skipDataDelete, next) { + if (!objectMD) { + return next(null, mpuBucket, storedParts, destBucket, objectMD, skipDataDelete); } - // Continue with the operation regardless of deletion success/failure - // The important part is that we tried to clean up - return next(null, mpuBucket, storedParts, destBucket, objectMD, skipDataDelete); - }); - }, - function deleteData(mpuBucket, storedParts, destBucket, objectMD, - skipDataDelete, next) { - if (skipDataDelete) { - return next(null, mpuBucket, storedParts, destBucket); - } - // The locations were sent to metadata as an array - // under partLocations. Pull the partLocations. - const locations = storedParts.flatMap(item => item.value.partLocations); - if (locations.length === 0) { - return next(null, mpuBucket, storedParts, destBucket); - } - // Add object data locations if they exist - if (objectMD?.location) { - const existingLocations = new Set(locations.map(loc => loc.key)); - const remainingObjectLocations = objectMD. - location.filter(loc => !existingLocations.has(loc.key)); - locations.push(...remainingObjectLocations); - } + log.debug('Object has existing metadata, deleting them', { + method: 'abortMultipartUpload', + bucketName, + objectKey, + uploadId, + versionId: objectMD.versionId, + }); + + const options = preprocessingVersioningDelete( + bucketName, + destBucket, + objectMD, + objectMD.versionId, + config.nullVersionCompatMode, + ); + options.replayId = uploadId; + options.overheadField = constants.overheadField; + // This is a ghost metadata, without data loss, so skip oplog + options.doesNotNeedOpogUpdate = true; - return async.eachLimit(locations, 5, (loc, cb) => { - data.delete(loc, log, err => { + return metadata.deleteObjectMD(bucketName, objectKey, options, log, err => { if (err) { - log.warn('delete ObjectPart failed', { err }); - } - cb(); - }); - }, () => { - const length = storedParts.reduce((length, loc) => length + loc.value.Size, 0); - return validateQuotas(request, destBucket, request.accountQuotas, - ['objectDelete'], 'objectDelete', -length, false, log, err => { - if (err) { - // Ignore error, as the data has been deleted already: only inflight count - // has not been updated, and will be eventually consistent anyway - log.warn('failed to update inflights', { + // Handle concurrent deletion of this object metadata + if (err.is?.NoSuchKey) { + log.debug('object metadata already deleted or does not exist', { method: 'abortMultipartUpload', - locations, - error: err, + bucketName, + objectKey, + versionId: objectMD.versionId, }); + } else { + log.error('error deleting object metadata', { error: err }); } - next(null, mpuBucket, storedParts, destBucket); - }); - }); - }, - function deleteShadowObjectMetadata(mpuBucket, storedParts, destBucket, next) { - let splitter = constants.splitter; - // BACKWARD: Remove to remove the old splitter - if (mpuBucket.getMdBucketModelVersion() < 2) { - splitter = constants.oldSplitter; - } - // Reconstruct mpuOverviewKey - const mpuOverviewKey = - `overview${splitter}${objectKey}${splitter}${uploadId}`; + } + // Continue with the operation regardless of deletion success/failure + // The important part is that we tried to clean up + return next(null, mpuBucket, storedParts, destBucket, objectMD, skipDataDelete); + }); + }, + function deleteData(mpuBucket, storedParts, destBucket, objectMD, skipDataDelete, next) { + if (skipDataDelete) { + return next(null, mpuBucket, storedParts, destBucket); + } + // The locations were sent to metadata as an array + // under partLocations. Pull the partLocations. + const locations = storedParts.flatMap(item => item.value.partLocations); + if (locations.length === 0) { + return next(null, mpuBucket, storedParts, destBucket); + } + + // Add object data locations if they exist + if (objectMD?.location) { + const existingLocations = new Set(locations.map(loc => loc.key)); + const remainingObjectLocations = objectMD.location.filter(loc => !existingLocations.has(loc.key)); + locations.push(...remainingObjectLocations); + } + + return async.eachLimit( + locations, + 5, + (loc, cb) => { + data.delete(loc, log, err => { + if (err) { + log.warn('delete ObjectPart failed', { err }); + } + cb(); + }); + }, + () => { + const length = storedParts.reduce((length, loc) => length + loc.value.Size, 0); + return validateQuotas( + request, + destBucket, + request.accountQuotas, + ['objectDelete'], + 'objectDelete', + -length, + false, + log, + err => { + if (err) { + // Ignore error, as the data has been deleted already: only inflight count + // has not been updated, and will be eventually consistent anyway + log.warn('failed to update inflights', { + method: 'abortMultipartUpload', + locations, + error: err, + }); + } + next(null, mpuBucket, storedParts, destBucket); + }, + ); + }, + ); + }, + function deleteShadowObjectMetadata(mpuBucket, storedParts, destBucket, next) { + let splitter = constants.splitter; + // BACKWARD: Remove to remove the old splitter + if (mpuBucket.getMdBucketModelVersion() < 2) { + splitter = constants.oldSplitter; + } + // Reconstruct mpuOverviewKey + const mpuOverviewKey = `overview${splitter}${objectKey}${splitter}${uploadId}`; - // Get the sum of all part sizes to include in pushMetric object - const partSizeSum = storedParts.map(item => item.value.Size) - .reduce((currPart, nextPart) => currPart + nextPart, 0); - const keysToDelete = storedParts.map(item => item.key); - keysToDelete.push(mpuOverviewKey); - services.batchDeleteObjectMetadata(mpuBucket.getName(), - keysToDelete, log, err => next(err, destBucket, partSizeSum)); - }, - ], callback); + // Get the sum of all part sizes to include in pushMetric object + const partSizeSum = storedParts + .map(item => item.value.Size) + .reduce((currPart, nextPart) => currPart + nextPart, 0); + const keysToDelete = storedParts.map(item => item.key); + keysToDelete.push(mpuOverviewKey); + services.batchDeleteObjectMetadata(mpuBucket.getName(), keysToDelete, log, err => + next(err, destBucket, partSizeSum), + ); + }, + ], + callback, + ); } module.exports = abortMultipartUpload; diff --git a/tests/unit/api/apiUtils/object/abortMultipartUpload.spec.js b/tests/unit/api/apiUtils/object/abortMultipartUpload.spec.js index e9c1965b9d..350d0c056b 100644 --- a/tests/unit/api/apiUtils/object/abortMultipartUpload.spec.js +++ b/tests/unit/api/apiUtils/object/abortMultipartUpload.spec.js @@ -58,19 +58,33 @@ describe('abortMultipartUpload', () => { beforeEach(() => { cleanup(); - sinon.stub(data, 'abortMPU').callsFake( - (objectKey, uploadId, location, bucketName, request, - destBucket, locationConstraintCheckFn, log, callback) => callback(null, false) - ); + sinon + .stub(data, 'abortMPU') + .callsFake( + ( + objectKey, + uploadId, + location, + bucketName, + request, + destBucket, + locationConstraintCheckFn, + log, + callback, + ) => callback(null, false), + ); sinon.stub(data, 'delete').yields(null); sinon.stub(quotaUtils, 'validateQuotas').yields(null); - sinon.stub(services, 'metadataValidateMultipart') - .yields(null, { + sinon.stub(services, 'metadataValidateMultipart').yields( + null, + { getName: () => 'mpu-shadow-bucket', getMdBucketModelVersion: () => 2, isVersioningEnabled: () => true, - }, { controllingLocationConstraint: 'us-east-1' }); + }, + { controllingLocationConstraint: 'us-east-1' }, + ); sinon.stub(services, 'getMPUparts').yields(null, { Contents: [] }); sinon.stub(services, 'batchDeleteObjectMetadata').yields(null); @@ -81,48 +95,75 @@ describe('abortMultipartUpload', () => { }); function createBucketAndMPU(versioned, callback) { - async.waterfall([ - next => bucketPut(authInfo, bucketRequest, log, err => next(err)), - next => { - if (versioned) { - return bucketPutVersioning(authInfo, enableVersioningRequest, log, err => next(err)); - } - return next(); - }, - next => initiateMultipartUpload(authInfo, initiateRequest, log, (err, result) => next(err, result)), - (result, next) => parseString(result, (err, json) => - next(err, json.InitiateMultipartUploadResult.UploadId[0])), - ], callback); + async.waterfall( + [ + next => bucketPut(authInfo, bucketRequest, log, err => next(err)), + next => { + if (versioned) { + return bucketPutVersioning(authInfo, enableVersioningRequest, log, err => next(err)); + } + return next(); + }, + next => initiateMultipartUpload(authInfo, initiateRequest, log, (err, result) => next(err, result)), + (result, next) => + parseString(result, (err, json) => next(err, json.InitiateMultipartUploadResult.UploadId[0])), + ], + callback, + ); } describe('basic functionality', () => { it('should successfully abort multipart upload', done => { createBucketAndMPU(false, (err, uploadId) => { assert.ifError(err); - abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log, err => { - assert.strictEqual(err, null); - done(); - }, { ...abortRequest, query: { uploadId } }); + abortMultipartUpload( + authInfo, + bucketName, + objectKey, + uploadId, + log, + err => { + assert.strictEqual(err, null); + done(); + }, + { ...abortRequest, query: { uploadId } }, + ); }); }); it('should return error for non-existent bucket', done => { - abortMultipartUpload(authInfo, 'non-existent-bucket', objectKey, 'fake-upload-id', log, err => { - assert(err); - assert.strictEqual(err.is.NoSuchBucket, true); - done(); - }, abortRequest); + abortMultipartUpload( + authInfo, + 'non-existent-bucket', + objectKey, + 'fake-upload-id', + log, + err => { + assert(err); + assert.strictEqual(err.is.NoSuchBucket, true); + done(); + }, + abortRequest, + ); }); it('should return error for non-existent upload', done => { services.metadataValidateMultipart.yields(errors.NoSuchUpload); bucketPut(authInfo, bucketRequest, log, err => { assert.ifError(err); - abortMultipartUpload(authInfo, bucketName, objectKey, 'fake-upload-id', log, err => { - assert(err); - assert.strictEqual(err.is.NoSuchUpload, true); - done(); - }, abortRequest); + abortMultipartUpload( + authInfo, + bucketName, + objectKey, + 'fake-upload-id', + log, + err => { + assert(err); + assert.strictEqual(err.is.NoSuchUpload, true); + done(); + }, + abortRequest, + ); }); }); @@ -134,10 +175,18 @@ describe('abortMultipartUpload', () => { }); createBucketAndMPU(false, (err, uploadId) => { assert.ifError(err); - abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log, err => { - assert.deepStrictEqual(err, testError); - done(); - }, { ...abortRequest, query: { uploadId } }); + abortMultipartUpload( + authInfo, + bucketName, + objectKey, + uploadId, + log, + err => { + assert.deepStrictEqual(err, testError); + done(); + }, + { ...abortRequest, query: { uploadId } }, + ); }); }); }); @@ -146,31 +195,46 @@ describe('abortMultipartUpload', () => { it('should delete part data when aborting', done => { createBucketAndMPU(false, (err, uploadId) => { assert.ifError(err); - const partRequest = new DummyRequest({ - bucketName, objectKey, namespace: 'default', - url: `/${objectKey}?partNumber=1&uploadId=${uploadId}`, - headers: { host: `${bucketName}.s3.amazonaws.com` }, - query: { partNumber: '1', uploadId }, - calculatedHash: crypto.createHash('md5').update(postBody).digest('hex'), - }, postBody); + const partRequest = new DummyRequest( + { + bucketName, + objectKey, + namespace: 'default', + url: `/${objectKey}?partNumber=1&uploadId=${uploadId}`, + headers: { host: `${bucketName}.s3.amazonaws.com` }, + query: { partNumber: '1', uploadId }, + calculatedHash: crypto.createHash('md5').update(postBody).digest('hex'), + }, + postBody, + ); objectPutPart(authInfo, partRequest, undefined, log, err => { assert.ifError(err); services.getMPUparts.yields(null, { - Contents: [{ - key: `1${uploadId}`, - value: { - Size: 11, - partLocations: [{ key: 'a-key' }], + Contents: [ + { + key: `1${uploadId}`, + value: { + Size: 11, + partLocations: [{ key: 'a-key' }], + }, }, - }], + ], }); - abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log, err => { - assert.strictEqual(err, null); - sinon.assert.called(data.delete); - done(); - }, { ...abortRequest, query: { uploadId } }); + abortMultipartUpload( + authInfo, + bucketName, + objectKey, + uploadId, + log, + err => { + assert.strictEqual(err, null); + sinon.assert.called(data.delete); + done(); + }, + { ...abortRequest, query: { uploadId } }, + ); }); }); }); @@ -192,11 +256,19 @@ describe('abortMultipartUpload', () => { it('should NOT search for orphans if master object does not exist', done => { bucketPutVersioning(authInfo, enableVersioningRequest, log, err => { assert.ifError(err); - abortMultipartUpload(authInfo, bucketName, objectKey, 'any-id', log, err => { - assert.ifError(err); - sinon.assert.notCalled(findObjectVersionStub); - done(); - }, abortRequest); + abortMultipartUpload( + authInfo, + bucketName, + objectKey, + 'any-id', + log, + err => { + assert.ifError(err); + sinon.assert.notCalled(findObjectVersionStub); + done(); + }, + abortRequest, + ); }); }); @@ -207,14 +279,22 @@ describe('abortMultipartUpload', () => { getOwner: () => 'testCanonicalId', getName: () => bucketName, }; - const mockMasterMD = { 'uploadId': 'master-id' }; + const mockMasterMD = { uploadId: 'master-id' }; standardMetadataValidateStub.yields(null, mockBucket, mockMasterMD); - abortMultipartUpload(authInfo, bucketName, objectKey, 'abort-id', log, err => { - assert.ifError(err); - sinon.assert.notCalled(findObjectVersionStub); - done(); - }, { ...abortRequest, query: { uploadId: 'abort-id' } }); + abortMultipartUpload( + authInfo, + bucketName, + objectKey, + 'abort-id', + log, + err => { + assert.ifError(err); + sinon.assert.notCalled(findObjectVersionStub); + done(); + }, + { ...abortRequest, query: { uploadId: 'abort-id' } }, + ); }); it('should proceed without cleanup if finding object version fails', done => { @@ -228,16 +308,24 @@ describe('abortMultipartUpload', () => { getOwner: () => 'testCanonicalId', getName: () => bucketName, }; - const mockMasterMD = { 'uploadId': 'master-id' }; + const mockMasterMD = { uploadId: 'master-id' }; standardMetadataValidateStub.yields(null, mockBucket, mockMasterMD); - abortMultipartUpload(authInfo, bucketName, objectKey, 'abort-id', log, err => { - assert.ifError(err); - sinon.assert.calledOnce(findObjectVersionStub); - sinon.assert.notCalled(metadataGetObjectStub); - sinon.assert.notCalled(deleteObjectMDStub); - done(); - }, { ...abortRequest, query: { uploadId: 'abort-id' } }); + abortMultipartUpload( + authInfo, + bucketName, + objectKey, + 'abort-id', + log, + err => { + assert.ifError(err); + sinon.assert.calledOnce(findObjectVersionStub); + sinon.assert.notCalled(metadataGetObjectStub); + sinon.assert.notCalled(deleteObjectMDStub); + done(); + }, + { ...abortRequest, query: { uploadId: 'abort-id' } }, + ); }); it('should proceed without cleanup if finding object version returns null', done => { @@ -250,16 +338,24 @@ describe('abortMultipartUpload', () => { getOwner: () => 'testCanonicalId', getName: () => bucketName, }; - const mockMasterMD = { 'uploadId': 'master-id' }; + const mockMasterMD = { uploadId: 'master-id' }; standardMetadataValidateStub.yields(null, mockBucket, mockMasterMD); - abortMultipartUpload(authInfo, bucketName, objectKey, 'abort-id', log, err => { - assert.ifError(err); - sinon.assert.calledOnce(findObjectVersionStub); - sinon.assert.notCalled(metadataGetObjectStub); - sinon.assert.notCalled(deleteObjectMDStub); - done(); - }, { ...abortRequest, query: { uploadId: 'abort-id' } }); + abortMultipartUpload( + authInfo, + bucketName, + objectKey, + 'abort-id', + log, + err => { + assert.ifError(err); + sinon.assert.calledOnce(findObjectVersionStub); + sinon.assert.notCalled(metadataGetObjectStub); + sinon.assert.notCalled(deleteObjectMDStub); + done(); + }, + { ...abortRequest, query: { uploadId: 'abort-id' } }, + ); }); it('should proceed without cleanup if found version getObject fails', done => { @@ -274,17 +370,25 @@ describe('abortMultipartUpload', () => { getOwner: () => 'testCanonicalId', getName: () => bucketName, }; - const mockMasterMD = { 'uploadId': 'master-id' }; + const mockMasterMD = { uploadId: 'master-id' }; standardMetadataValidateStub.yields(null, mockBucket, mockMasterMD); - abortMultipartUpload(authInfo, bucketName, objectKey, 'abort-id', log, err => { - assert.ifError(err); - sinon.assert.calledOnce(findObjectVersionStub); - sinon.assert.calledOnce(metadataGetObjectStub); - sinon.assert.calledWith(metadataGetObjectStub, bucketName, objectKey, 'orphan-vid', null, log); - sinon.assert.notCalled(deleteObjectMDStub); - done(); - }, { ...abortRequest, query: { uploadId: 'abort-id' } }); + abortMultipartUpload( + authInfo, + bucketName, + objectKey, + 'abort-id', + log, + err => { + assert.ifError(err); + sinon.assert.calledOnce(findObjectVersionStub); + sinon.assert.calledOnce(metadataGetObjectStub); + sinon.assert.calledWith(metadataGetObjectStub, bucketName, objectKey, 'orphan-vid', null, log); + sinon.assert.notCalled(deleteObjectMDStub); + done(); + }, + { ...abortRequest, query: { uploadId: 'abort-id' } }, + ); }); it('should delete the correct orphaned object version', done => { @@ -299,17 +403,24 @@ describe('abortMultipartUpload', () => { getName: () => bucketName, getVersioningConfiguration: () => ({ Status: 'Enabled' }), }; - const mockMasterMD = { 'uploadId': 'master-id' }; + const mockMasterMD = { uploadId: 'master-id' }; standardMetadataValidateStub.yields(null, mockBucket, mockMasterMD); - abortMultipartUpload(authInfo, bucketName, objectKey, 'abort-id', log, err => { - assert.ifError(err); - sinon.assert.calledOnce(deleteObjectMDStub); - assert.strictEqual(deleteObjectMDStub.getCall(0).args[2].versionId, 'orphan-vid'); - assert.deepStrictEqual( - deleteObjectMDStub.getCall(0).args[2].overheadField, overheadField); - done(); - }, { ...abortRequest, query: { uploadId: 'abort-id' } }); + abortMultipartUpload( + authInfo, + bucketName, + objectKey, + 'abort-id', + log, + err => { + assert.ifError(err); + sinon.assert.calledOnce(deleteObjectMDStub); + assert.strictEqual(deleteObjectMDStub.getCall(0).args[2].versionId, 'orphan-vid'); + assert.deepStrictEqual(deleteObjectMDStub.getCall(0).args[2].overheadField, overheadField); + done(); + }, + { ...abortRequest, query: { uploadId: 'abort-id' } }, + ); }); it('should proceed if orphaned object version is already deleted (NoSuchKey)', done => { @@ -324,14 +435,22 @@ describe('abortMultipartUpload', () => { getName: () => bucketName, getVersioningConfiguration: () => ({ Status: 'Enabled' }), }; - const mockMasterMD = { 'uploadId': 'master-id' }; + const mockMasterMD = { uploadId: 'master-id' }; standardMetadataValidateStub.yields(null, mockBucket, mockMasterMD); - abortMultipartUpload(authInfo, bucketName, objectKey, 'abort-id', log, err => { - assert.ifError(err); - sinon.assert.calledOnce(deleteObjectMDStub); - done(); - }, { ...abortRequest, query: { uploadId: 'abort-id' } }); + abortMultipartUpload( + authInfo, + bucketName, + objectKey, + 'abort-id', + log, + err => { + assert.ifError(err); + sinon.assert.calledOnce(deleteObjectMDStub); + done(); + }, + { ...abortRequest, query: { uploadId: 'abort-id' } }, + ); }); }); }); From 4628dd2adfeefc090ab5bc0077b436ed128930cf Mon Sep 17 00:00:00 2001 From: Dimitrios Vasilas Date: Wed, 2 Sep 2026 16:18:50 +0300 Subject: [PATCH 3/3] Bump version to 9.3.20 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7f2b120b93..cb07ebb836 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zenko/cloudserver", - "version": "9.3.19", + "version": "9.3.20", "description": "Zenko CloudServer, an open-source Node.js implementation of a server handling the Amazon S3 protocol", "main": "index.js", "engines": {