diff --git a/lib/api/objectCopy.js b/lib/api/objectCopy.js index 02b0a31d5b..e99a9e7c06 100644 --- a/lib/api/objectCopy.js +++ b/lib/api/objectCopy.js @@ -7,18 +7,15 @@ const validateHeaders = s3middleware.validateConditionalHeaders; const constants = require('../../constants'); const collectCorsHeaders = require('../utilities/collectCorsHeaders'); -const locationConstraintCheck - = require('./apiUtils/object/locationConstraintCheck'); -const { checkQueryVersionId, versioningPreprocessing, decodeVID } - = require('./apiUtils/object/versioning'); +const locationConstraintCheck = require('./apiUtils/object/locationConstraintCheck'); +const { checkQueryVersionId, versioningPreprocessing, decodeVID } = require('./apiUtils/object/versioning'); const getReplicationInfo = require('./apiUtils/object/getReplicationInfo'); const { data } = require('../data/wrapper'); const services = require('../services'); const { pushMetric } = require('../utapi/utilities'); const removeAWSChunked = require('./apiUtils/object/removeAWSChunked'); const { standardMetadataValidateBucketAndObj } = require('../metadata/metadataUtils'); -const validateWebsiteHeader = require('./apiUtils/object/websiteServing') - .validateWebsiteHeader; +const validateWebsiteHeader = require('./apiUtils/object/websiteServing').validateWebsiteHeader; const { config } = require('../Config'); const monitoring = require('../utilities/monitoringHandler'); const applyZenkoUserMD = require('./apiUtils/object/applyZenkoUserMD'); @@ -32,8 +29,8 @@ const { initializeInternalLogRequestQueue, queueInternalLogRequest } = require(' const versionIdUtils = versioning.VersionID; const locationHeader = constants.objectLocationConstraintHeader; const versioningNotImplBackends = constants.versioningNotImplBackends; -const externalVersioningErrorMessage = 'We do not currently support putting ' + -'a versioned object to a location-constraint of type AWS or Azure or GCP.'; +const externalVersioningErrorMessage = + 'We do not currently support putting ' + 'a versioned object to a location-constraint of type AWS or Azure or GCP.'; /** * Preps metadata to be saved (based on copy or replace request header) @@ -53,8 +50,18 @@ const externalVersioningErrorMessage = 'We do not currently support putting ' + * - sourceLocationConstraintName {string} - location type of the source * - OR error */ -function _prepMetadata(request, sourceObjMD, headers, sourceIsDestination, - authInfo, objectKey, sourceBucketMD, destBucketMD, sourceVersionId, log) { +function _prepMetadata( + request, + sourceObjMD, + headers, + sourceIsDestination, + authInfo, + objectKey, + sourceBucketMD, + destBucketMD, + sourceVersionId, + log, +) { let whichMetadata = headers['x-amz-metadata-directive']; // Default is COPY whichMetadata = whichMetadata === undefined ? 'COPY' : whichMetadata; @@ -65,45 +72,47 @@ function _prepMetadata(request, sourceObjMD, headers, sourceIsDestination, // Default is COPY whichTagging = whichTagging === undefined ? 'COPY' : whichTagging; if (whichTagging !== 'COPY' && whichTagging !== 'REPLACE') { - return { error: errorInstances.InvalidArgument - .customizeDescription('Unknown tagging directive') }; + return { error: errorInstances.InvalidArgument.customizeDescription('Unknown tagging directive') }; } const overrideMetadata = {}; if (headers['x-amz-server-side-encryption']) { - overrideMetadata['x-amz-server-side-encryption'] = - headers['x-amz-server-side-encryption']; + overrideMetadata['x-amz-server-side-encryption'] = headers['x-amz-server-side-encryption']; } - if (headers['x-amz-storage-class']) { // TODO: remove in CLDSRV-639 - overrideMetadata['x-amz-storage-class'] = - headers['x-amz-storage-class']; + if (headers['x-amz-storage-class']) { + // TODO: remove in CLDSRV-639 + overrideMetadata['x-amz-storage-class'] = headers['x-amz-storage-class']; } if (headers['x-amz-website-redirect-location']) { - overrideMetadata['x-amz-website-redirect-location'] = - headers['x-amz-website-redirect-location']; + overrideMetadata['x-amz-website-redirect-location'] = headers['x-amz-website-redirect-location']; } - const retentionHeaders = headers['x-amz-object-lock-mode'] - && headers['x-amz-object-lock-retain-until-date']; + const retentionHeaders = headers['x-amz-object-lock-mode'] && headers['x-amz-object-lock-retain-until-date']; const legalHoldHeader = headers['x-amz-object-lock-legal-hold']; - if ((retentionHeaders || legalHoldHeader) - && !destBucketMD.isObjectLockEnabled()) { - return { error: errorInstances.InvalidRequest.customizeDescription( - 'Bucket is missing ObjectLockConfiguration') }; + if ((retentionHeaders || legalHoldHeader) && !destBucketMD.isObjectLockEnabled()) { + return { + error: errorInstances.InvalidRequest.customizeDescription('Bucket is missing ObjectLockConfiguration'), + }; } // Cannot copy from same source and destination if no MD // changed and no source version id - if (sourceIsDestination && whichMetadata === 'COPY' && - Object.keys(overrideMetadata).length === 0 && !sourceVersionId) { - return { error: errorInstances.InvalidRequest.customizeDescription('This copy' + - ' request is illegal because it is trying to copy an ' + - 'object to itself without changing the object\'s metadata, ' + - 'storage class, website redirect location or encryption ' + - 'attributes.') }; + if ( + sourceIsDestination && + whichMetadata === 'COPY' && + Object.keys(overrideMetadata).length === 0 && + !sourceVersionId + ) { + return { + error: errorInstances.InvalidRequest.customizeDescription( + 'This copy' + + ' request is illegal because it is trying to copy an ' + + "object to itself without changing the object's metadata, " + + 'storage class, website redirect location or encryption ' + + 'attributes.', + ), + }; } // If COPY, pull all x-amz-meta keys/values from source object // Otherwise, pull all x-amz-meta keys/values from request headers - const userMetadata = whichMetadata === 'COPY' ? - getMetaHeaders(sourceObjMD) : - getMetaHeaders(headers); + const userMetadata = whichMetadata === 'COPY' ? getMetaHeaders(sourceObjMD) : getMetaHeaders(headers); if (userMetadata instanceof Error) { log.debug('user metadata validation failed', { error: userMetadata, @@ -117,28 +126,23 @@ function _prepMetadata(request, sourceObjMD, headers, sourceIsDestination, // If metadataDirective is: // - 'COPY' and source object has a location constraint in its metadata // we use the bucket destination location constraint - if (whichMetadata === 'COPY' - && userMetadata[locationHeader] - && destBucketMD.getLocationConstraint()) { + if (whichMetadata === 'COPY' && userMetadata[locationHeader] && destBucketMD.getLocationConstraint()) { userMetadata[locationHeader] = destBucketMD.getLocationConstraint(); } - const backendInfoObjSource = locationConstraintCheck(request, - sourceObjMD, sourceBucketMD, log); + const backendInfoObjSource = locationConstraintCheck(request, sourceObjMD, sourceBucketMD, log); if (backendInfoObjSource.err) { return { error: backendInfoObjSource.err }; } const sourceLocationConstraintName = backendInfoObjSource.controllingLC; - const backendInfoObjDest = locationConstraintCheck(request, - userMetadata, destBucketMD, log); + const backendInfoObjDest = locationConstraintCheck(request, userMetadata, destBucketMD, log); if (backendInfoObjDest.err) { return { error: backendInfoObjDest.err }; } const destLocationConstraintName = backendInfoObjDest.controllingLC; // If location constraint header is not included, locations match - const locationMatch = - sourceLocationConstraintName === destLocationConstraintName; + const locationMatch = sourceLocationConstraintName === destLocationConstraintName; // If tagging directive is REPLACE but you don't specify any // tags in the request, the destination object will @@ -155,8 +159,7 @@ function _prepMetadata(request, sourceObjMD, headers, sourceIsDestination, // If COPY, pull the necessary headers from source object // Otherwise, pull them from request headers - const headersToStoreSource = whichMetadata === 'COPY' ? - sourceObjMD : headers; + const headersToStoreSource = whichMetadata === 'COPY' ? sourceObjMD : headers; const storeMetadataParams = { objectKey, @@ -169,16 +172,14 @@ function _prepMetadata(request, sourceObjMD, headers, sourceIsDestination, contentMD5: sourceObjMD['content-md5'], cacheControl: headersToStoreSource['cache-control'], contentDisposition: headersToStoreSource['content-disposition'], - contentEncoding: - removeAWSChunked(headersToStoreSource['content-encoding']), + contentEncoding: removeAWSChunked(headersToStoreSource['content-encoding']), dataStoreName: destLocationConstraintName, expires: headersToStoreSource.expires, overrideMetadata, lastModifiedDate: new Date().toJSON(), tagging, taggingCopy, - replicationInfo: getReplicationInfo(config, - objectKey, destBucketMD, false, sourceObjMD['content-length']), + replicationInfo: getReplicationInfo(config, objectKey, destBucketMD, false, sourceObjMD['content-length']), locationMatch, originOp: 's3:ObjectCreated:Copy', }; @@ -198,8 +199,7 @@ function _prepMetadata(request, sourceObjMD, headers, sourceIsDestination, storeMetadataParams.bucketOwnerId = destBucketMD.getOwner(); } - return { storeMetadataParams, sourceLocationConstraintName, - backendInfoDest: backendInfoObjDest.backendInfo }; + return { storeMetadataParams, sourceLocationConstraintName, backendInfoDest: backendInfoObjDest.backendInfo }; } /** @@ -215,8 +215,7 @@ function _prepMetadata(request, sourceObjMD, headers, sourceIsDestination, * @param {function} callback - final callback to call with the result * @return {undefined} */ -function objectCopy(authInfo, request, sourceBucket, - sourceObject, sourceVersionId, log, callback) { +function objectCopy(authInfo, request, sourceBucket, sourceObject, sourceVersionId, log, callback) { log.debug('processing request', { method: 'objectCopy' }); const destBucketName = request.bucketName; const destObjectKey = request.objectKey; @@ -226,8 +225,7 @@ function objectCopy(authInfo, request, sourceBucket, return callback(keyLengthError); } - const sourceIsDestination = - destBucketName === sourceBucket && destObjectKey === sourceObject; + const sourceIsDestination = destBucketName === sourceBucket && destObjectKey === sourceObject; const valGetParams = { authInfo, bucketName: sourceBucket, @@ -259,432 +257,574 @@ function objectCopy(authInfo, request, sourceBucket, namespace: request.namespace, objectKey: destObjectKey, }; - const websiteRedirectHeader = - request.headers['x-amz-website-redirect-location']; + const websiteRedirectHeader = request.headers['x-amz-website-redirect-location']; const responseHeaders = {}; - if (request.headers['x-amz-storage-class'] && - !constants.validStorageClasses.includes(request.headers['x-amz-storage-class'])) { + if ( + request.headers['x-amz-storage-class'] && + !constants.validStorageClasses.includes(request.headers['x-amz-storage-class']) + ) { log.trace('invalid storage-class header'); - monitoring.promMetrics('PUT', destBucketName, - errorInstances.InvalidStorageClass.code, 'copyObject'); + monitoring.promMetrics('PUT', destBucketName, errorInstances.InvalidStorageClass.code, 'copyObject'); return callback(errors.InvalidStorageClass); } if (!validateWebsiteHeader(websiteRedirectHeader)) { const err = errors.InvalidRedirectLocation; - log.debug('invalid x-amz-website-redirect-location' + - `value ${websiteRedirectHeader}`, { error: err }); - monitoring.promMetrics( - 'PUT', destBucketName, err.code, 'copyObject'); + log.debug('invalid x-amz-website-redirect-location' + `value ${websiteRedirectHeader}`, { error: err }); + monitoring.promMetrics('PUT', destBucketName, err.code, 'copyObject'); return callback(err); } const queryContainsVersionId = checkQueryVersionId(request.query); if (queryContainsVersionId instanceof Error) { return callback(queryContainsVersionId); } - return async.waterfall([ - function checkDestAuth(next) { - return standardMetadataValidateBucketAndObj(valPutParams, request.actionImplicitDenies, log, - (err, destBucketMD, destObjMD) => - updateEncryption(err, destBucketMD, destObjMD, destObjectKey, log, { skipObject: true }, - (err, destBucketMD, destObjMD) => { - if (err) { - log.debug('error validating put part of request', - { error: err }); - return next(err, destBucketMD); - } - const flag = destBucketMD.hasDeletedFlag() - || destBucketMD.hasTransientFlag(); - if (flag) { - log.trace('deleted flag or transient flag ' + - 'on destination bucket', { flag }); - return next(errors.NoSuchBucket); - } - return next(null, destBucketMD, destObjMD); - })); - }, - function checkSourceAuthorization(destBucketMD, destObjMD, next) { - return standardMetadataValidateBucketAndObj({ - ...valGetParams, - destObjMD, - serverAccessLogOptions: { copySource: true }, - }, request.actionImplicitDenies, log, - (err, sourceBucketMD, sourceObjMD) => { - if (err) { - log.debug('error validating get part of request', - { error: err }); - // eslint-disable-next-line no-param-reassign - request.sourceServerAccessLog && (request.sourceServerAccessLog.error = err); - return next(err, null, destBucketMD); - } - if (!sourceObjMD) { - const err = sourceVersionId ? errors.NoSuchVersion : - errors.NoSuchKey; - log.debug('no source object', { sourceObject }); - // eslint-disable-next-line no-param-reassign - request.sourceServerAccessLog && (request.sourceServerAccessLog.error = err); - return next(err, null, destBucketMD); - } - // check if object data is in a cold storage - const coldErr = verifyColdObjectAvailable(sourceObjMD); - if (coldErr) { - // eslint-disable-next-line no-param-reassign - request.sourceServerAccessLog && (request.sourceServerAccessLog.error = coldErr); - return next(coldErr, null); - } - if (sourceObjMD.isDeleteMarker) { - log.debug('delete marker on source object', - { sourceObject }); - let err; - if (sourceVersionId) { - err = errorInstances.InvalidRequest - .customizeDescription('The source of a copy ' + - 'request may not specifically refer to a delete' + - 'marker by version id.'); - } else { - // if user specifies a key in a versioned source bucket - // without specifying a version, and the object has - // a delete marker, return NoSuchKey - err = errors.NoSuchKey; + return async.waterfall( + [ + function checkDestAuth(next) { + return standardMetadataValidateBucketAndObj( + valPutParams, + request.actionImplicitDenies, + log, + (err, destBucketMD, destObjMD) => + updateEncryption( + err, + destBucketMD, + destObjMD, + destObjectKey, + log, + { skipObject: true }, + (err, destBucketMD, destObjMD) => { + if (err) { + log.debug('error validating put part of request', { error: err }); + return next(err, destBucketMD); + } + const flag = destBucketMD.hasDeletedFlag() || destBucketMD.hasTransientFlag(); + if (flag) { + log.trace('deleted flag or transient flag ' + 'on destination bucket', { flag }); + return next(errors.NoSuchBucket); + } + return next(null, destBucketMD, destObjMD); + }, + ), + ); + }, + function checkSourceAuthorization(destBucketMD, destObjMD, next) { + return standardMetadataValidateBucketAndObj( + { + ...valGetParams, + destObjMD, + serverAccessLogOptions: { copySource: true }, + }, + request.actionImplicitDenies, + log, + (err, sourceBucketMD, sourceObjMD) => { + if (err) { + log.debug('error validating get part of request', { error: err }); + // eslint-disable-next-line no-param-reassign + request.sourceServerAccessLog && (request.sourceServerAccessLog.error = err); + return next(err, null, destBucketMD); } - // eslint-disable-next-line no-param-reassign - request.sourceServerAccessLog && (request.sourceServerAccessLog.error = err); - return next(err, destBucketMD); - } - const headerValResult = - validateHeaders(request.headers, - sourceObjMD['last-modified'], - sourceObjMD['content-md5']); - if (headerValResult.error) { - request.sourceServerAccessLog + if (!sourceObjMD) { + const err = sourceVersionId ? errors.NoSuchVersion : errors.NoSuchKey; + log.debug('no source object', { sourceObject }); // eslint-disable-next-line no-param-reassign - && (request.sourceServerAccessLog.error = errors.PreconditionFailed); - return next(errors.PreconditionFailed, destBucketMD); - } - const { storeMetadataParams, error: metadataError, - sourceLocationConstraintName, backendInfoDest } = - _prepMetadata(request, sourceObjMD, request.headers, - sourceIsDestination, authInfo, destObjectKey, - sourceBucketMD, destBucketMD, sourceVersionId, log); - if (metadataError) { - // eslint-disable-next-line no-param-reassign - request.sourceServerAccessLog && (request.sourceServerAccessLog.error = metadataError); - return next(metadataError, destBucketMD); - } - if (storeMetadataParams.metaHeaders) { - dataStoreContext.metaHeaders = - storeMetadataParams.metaHeaders; - } + request.sourceServerAccessLog && (request.sourceServerAccessLog.error = err); + return next(err, null, destBucketMD); + } + // check if object data is in a cold storage + const coldErr = verifyColdObjectAvailable(sourceObjMD); + if (coldErr) { + // eslint-disable-next-line no-param-reassign + request.sourceServerAccessLog && (request.sourceServerAccessLog.error = coldErr); + return next(coldErr, null); + } + if (sourceObjMD.isDeleteMarker) { + log.debug('delete marker on source object', { sourceObject }); + let err; + if (sourceVersionId) { + err = errorInstances.InvalidRequest.customizeDescription( + 'The source of a copy ' + + 'request may not specifically refer to a delete' + + 'marker by version id.', + ); + } else { + // if user specifies a key in a versioned source bucket + // without specifying a version, and the object has + // a delete marker, return NoSuchKey + err = errors.NoSuchKey; + } + // eslint-disable-next-line no-param-reassign + request.sourceServerAccessLog && (request.sourceServerAccessLog.error = err); + return next(err, destBucketMD); + } + const sourceSize = parseInt(sourceObjMD['content-length'], 10); + if (sourceSize > constants.maximumAllowedUploadSize && !config.bypassMaxPutObjectSize) { + log.debug('copy source object too large', { sourceSize }); + const err = errorInstances.InvalidRequest.customizeDescription( + 'The specified copy source is larger than the maximum ' + + `allowable size for a copy source: ${constants.maximumAllowedUploadSize}`, + ); + // eslint-disable-next-line no-param-reassign + request.sourceServerAccessLog && (request.sourceServerAccessLog.error = err); + return next(err, destBucketMD); + } + const headerValResult = validateHeaders( + request.headers, + sourceObjMD['last-modified'], + sourceObjMD['content-md5'], + ); + if (headerValResult.error) { + request.sourceServerAccessLog && + // eslint-disable-next-line no-param-reassign + (request.sourceServerAccessLog.error = errors.PreconditionFailed); + return next(errors.PreconditionFailed, destBucketMD); + } + const { + storeMetadataParams, + error: metadataError, + sourceLocationConstraintName, + backendInfoDest, + } = _prepMetadata( + request, + sourceObjMD, + request.headers, + sourceIsDestination, + authInfo, + destObjectKey, + sourceBucketMD, + destBucketMD, + sourceVersionId, + log, + ); + if (metadataError) { + // eslint-disable-next-line no-param-reassign + request.sourceServerAccessLog && (request.sourceServerAccessLog.error = metadataError); + return next(metadataError, destBucketMD); + } + if (storeMetadataParams.metaHeaders) { + dataStoreContext.metaHeaders = storeMetadataParams.metaHeaders; + } - storeMetadataParams.overheadField = constants.overheadField; - - let dataLocator; - // If 0 byte object just set dataLocator to empty array - if (!sourceObjMD.location) { - dataLocator = []; - } else { - // To provide for backwards compatibility before - // md-model-version 2, need to handle cases where - // objMD.location is just a string - dataLocator = Array.isArray(sourceObjMD.location) ? - sourceObjMD.location : [{ key: sourceObjMD.location }]; - } + storeMetadataParams.overheadField = constants.overheadField; - if (sourceObjMD['x-amz-server-side-encryption']) { - for (let i = 0; i < dataLocator.length; i++) { - dataLocator[i].masterKeyId = sourceObjMD[ - 'x-amz-server-side-encryption-aws-kms-key-id']; - dataLocator[i].algorithm = - sourceObjMD['x-amz-server-side-encryption']; + let dataLocator; + // If 0 byte object just set dataLocator to empty array + if (!sourceObjMD.location) { + dataLocator = []; + } else { + // To provide for backwards compatibility before + // md-model-version 2, need to handle cases where + // objMD.location is just a string + dataLocator = Array.isArray(sourceObjMD.location) + ? sourceObjMD.location + : [{ key: sourceObjMD.location }]; + } + + if (sourceObjMD['x-amz-server-side-encryption']) { + for (let i = 0; i < dataLocator.length; i++) { + dataLocator[i].masterKeyId = sourceObjMD['x-amz-server-side-encryption-aws-kms-key-id']; + dataLocator[i].algorithm = sourceObjMD['x-amz-server-side-encryption']; + } } - } - // If the destination key already exists - if (destObjMD) { - // Re-use creation-time if we can - if (destObjMD['creation-time']) { - storeMetadataParams.creationTime = - destObjMD['creation-time']; - // Otherwise fallback to last-modified + // If the destination key already exists + if (destObjMD) { + // Re-use creation-time if we can + if (destObjMD['creation-time']) { + storeMetadataParams.creationTime = destObjMD['creation-time']; + // Otherwise fallback to last-modified + } else { + storeMetadataParams.creationTime = destObjMD['last-modified']; + } + // If this is a new key, create a new timestamp } else { - storeMetadataParams.creationTime = - destObjMD['last-modified']; + storeMetadataParams.creationTime = new Date().toJSON(); } - // If this is a new key, create a new timestamp - } else { - storeMetadataParams.creationTime = new Date().toJSON(); - } - return next(null, storeMetadataParams, dataLocator, - sourceBucketMD, destBucketMD, destObjMD, - sourceLocationConstraintName, backendInfoDest); - }); - }, - function getSSEConfiguration(storeMetadataParams, dataLocator, sourceBucketMD, - destBucketMD, destObjMD, sourceLocationConstraintName, - backendInfoDest, next) { - getObjectSSEConfiguration( - request.headers, - destBucketMD, - log, - (err, sseConfig) => - next(err, storeMetadataParams, dataLocator, sourceBucketMD, - destBucketMD, destObjMD, sourceLocationConstraintName, - backendInfoDest, sseConfig)); - }, - function goGetData(storeMetadataParams, dataLocator, sourceBucketMD, - destBucketMD, destObjMD, sourceLocationConstraintName, - backendInfoDest, serverSideEncryption, next) { - const vcfg = destBucketMD.getVersioningConfiguration(); - const isVersionedObj = vcfg && vcfg.Status === 'Enabled'; - const destLocationConstraintName = - storeMetadataParams.dataStoreName; - const needsEncryption = serverSideEncryption && !!serverSideEncryption.algo; - // skip if source and dest and location constraint the same and - // versioning is not enabled - // still send along serverSideEncryption info so algo - // and masterKeyId stored properly in metadata - if (sourceIsDestination && storeMetadataParams.locationMatch - && !isVersionedObj && !needsEncryption) { - return next(null, storeMetadataParams, dataLocator, destObjMD, - serverSideEncryption, destBucketMD); - } + return next( + null, + storeMetadataParams, + dataLocator, + sourceBucketMD, + destBucketMD, + destObjMD, + sourceLocationConstraintName, + backendInfoDest, + ); + }, + ); + }, + function getSSEConfiguration( + storeMetadataParams, + dataLocator, + sourceBucketMD, + destBucketMD, + destObjMD, + sourceLocationConstraintName, + backendInfoDest, + next, + ) { + getObjectSSEConfiguration(request.headers, destBucketMD, log, (err, sseConfig) => + next( + err, + storeMetadataParams, + dataLocator, + sourceBucketMD, + destBucketMD, + destObjMD, + sourceLocationConstraintName, + backendInfoDest, + sseConfig, + ), + ); + }, + function goGetData( + storeMetadataParams, + dataLocator, + sourceBucketMD, + destBucketMD, + destObjMD, + sourceLocationConstraintName, + backendInfoDest, + serverSideEncryption, + next, + ) { + const vcfg = destBucketMD.getVersioningConfiguration(); + const isVersionedObj = vcfg && vcfg.Status === 'Enabled'; + const destLocationConstraintName = storeMetadataParams.dataStoreName; + const needsEncryption = serverSideEncryption && !!serverSideEncryption.algo; + // skip if source and dest and location constraint the same and + // versioning is not enabled + // still send along serverSideEncryption info so algo + // and masterKeyId stored properly in metadata + if (sourceIsDestination && storeMetadataParams.locationMatch && !isVersionedObj && !needsEncryption) { + return next(null, storeMetadataParams, dataLocator, destObjMD, serverSideEncryption, destBucketMD); + } - // also skip if 0 byte object, unless location constraint is an - // external backend and differs from source, in which case put - // metadata to backend - let destLocationConstraintType; - if (config.backends.data === 'multiple') { - destLocationConstraintType = - config.getLocationConstraintType(destLocationConstraintName); - } - if (destLocationConstraintType && - versioningNotImplBackends[destLocationConstraintType] - && isVersionedObj) { - log.debug(externalVersioningErrorMessage, - { method: 'multipleBackendGateway', - error: errors.NotImplemented }); - return next(errorInstances.NotImplemented.customizeDescription( - externalVersioningErrorMessage), destBucketMD); - } - if (dataLocator.length === 0) { - if (!storeMetadataParams.locationMatch && - destLocationConstraintType && - constants.externalBackends[destLocationConstraintType]) { - return data.put(null, null, storeMetadataParams.size, - dataStoreContext, backendInfoDest, - log, (error, objectRetrievalInfo) => { - if (error) { - return next(error, destBucketMD); - } - const putResult = { - key: objectRetrievalInfo.key, - dataStoreName: objectRetrievalInfo. - dataStoreName, - dataStoreType: objectRetrievalInfo. - dataStoreType, - size: storeMetadataParams.size, - }; - const putResultArr = [putResult]; - return next(null, storeMetadataParams, putResultArr, - destObjMD, serverSideEncryption, destBucketMD); - }); + // also skip if 0 byte object, unless location constraint is an + // external backend and differs from source, in which case put + // metadata to backend + let destLocationConstraintType; + if (config.backends.data === 'multiple') { + destLocationConstraintType = config.getLocationConstraintType(destLocationConstraintName); } - return next(null, storeMetadataParams, dataLocator, destObjMD, - serverSideEncryption, destBucketMD); - } - const originalIdentityImpDenies = request.actionImplicitDenies; - // eslint-disable-next-line no-param-reassign - delete request.actionImplicitDenies; - return data.copyObject(request, sourceLocationConstraintName, - storeMetadataParams, dataLocator, dataStoreContext, - backendInfoDest, sourceBucketMD, destBucketMD, serverSideEncryption, log, - (err, results) => { - // eslint-disable-next-line no-param-reassign - request.actionImplicitDenies = originalIdentityImpDenies; - if (err) { - // eslint-disable-next-line no-param-reassign - request.sourceServerAccessLog && (request.sourceServerAccessLog.error = err); - return next(err, destBucketMD); + if ( + destLocationConstraintType && + versioningNotImplBackends[destLocationConstraintType] && + isVersionedObj + ) { + log.debug(externalVersioningErrorMessage, { + method: 'multipleBackendGateway', + error: errors.NotImplemented, + }); + return next( + errorInstances.NotImplemented.customizeDescription(externalVersioningErrorMessage), + destBucketMD, + ); } - return next(null, storeMetadataParams, results, - destObjMD, serverSideEncryption, destBucketMD); - }); - }, - function getVersioningInfo(storeMetadataParams, destDataGetInfoArr, - destObjMD, serverSideEncryption, destBucketMD, next) { - if (!destBucketMD.isVersioningEnabled() && destObjMD?.archive?.archiveInfo) { - // Ensure we trigger a "delete" event in the oplog for the previously archived object - // eslint-disable-next-line - storeMetadataParams.needOplogUpdate = 's3:ReplaceArchivedObject'; - } - return versioningPreprocessing(destBucketName, - destBucketMD, destObjectKey, destObjMD, log, - (err, options) => { - if (err) { - log.debug('error processing versioning info', - { error: err }); - return next(err, null, destBucketMD); + if (dataLocator.length === 0) { + if ( + !storeMetadataParams.locationMatch && + destLocationConstraintType && + constants.externalBackends[destLocationConstraintType] + ) { + return data.put( + null, + null, + storeMetadataParams.size, + dataStoreContext, + backendInfoDest, + log, + (error, objectRetrievalInfo) => { + if (error) { + return next(error, destBucketMD); + } + const putResult = { + key: objectRetrievalInfo.key, + dataStoreName: objectRetrievalInfo.dataStoreName, + dataStoreType: objectRetrievalInfo.dataStoreType, + size: storeMetadataParams.size, + }; + const putResultArr = [putResult]; + return next( + null, + storeMetadataParams, + putResultArr, + destObjMD, + serverSideEncryption, + destBucketMD, + ); + }, + ); } + return next(null, storeMetadataParams, dataLocator, destObjMD, serverSideEncryption, destBucketMD); + } + const originalIdentityImpDenies = request.actionImplicitDenies; + // eslint-disable-next-line no-param-reassign + delete request.actionImplicitDenies; + return data.copyObject( + request, + sourceLocationConstraintName, + storeMetadataParams, + dataLocator, + dataStoreContext, + backendInfoDest, + sourceBucketMD, + destBucketMD, + serverSideEncryption, + log, + (err, results) => { + // eslint-disable-next-line no-param-reassign + request.actionImplicitDenies = originalIdentityImpDenies; + if (err) { + // eslint-disable-next-line no-param-reassign + request.sourceServerAccessLog && (request.sourceServerAccessLog.error = err); + return next(err, destBucketMD); + } + return next(null, storeMetadataParams, results, destObjMD, serverSideEncryption, destBucketMD); + }, + ); + }, + function getVersioningInfo( + storeMetadataParams, + destDataGetInfoArr, + destObjMD, + serverSideEncryption, + destBucketMD, + next, + ) { + if (!destBucketMD.isVersioningEnabled() && destObjMD?.archive?.archiveInfo) { + // Ensure we trigger a "delete" event in the oplog for the previously archived object + // eslint-disable-next-line + storeMetadataParams.needOplogUpdate = 's3:ReplaceArchivedObject'; + } + return versioningPreprocessing( + destBucketName, + destBucketMD, + destObjectKey, + destObjMD, + log, + (err, options) => { + if (err) { + log.debug('error processing versioning info', { error: err }); + return next(err, null, destBucketMD); + } - const location = destDataGetInfoArr?.[0]?.dataStoreName; - if (location === destBucketMD.getLocationConstraint() && destBucketMD.isIngestionBucket()) { - // If the object is being written to the "ingested" storage location, keep the same - // versionId for consistency and to avoid creating an extra version when it gets - // ingested - const backendVersionId = decodeVID(destDataGetInfoArr[0].dataStoreVersionId); - if (!(backendVersionId instanceof Error)) { - options.versionId = backendVersionId; // eslint-disable-line no-param-reassign + const location = destDataGetInfoArr?.[0]?.dataStoreName; + if (location === destBucketMD.getLocationConstraint() && destBucketMD.isIngestionBucket()) { + // If the object is being written to the "ingested" storage location, keep the same + // versionId for consistency and to avoid creating an extra version when it gets + // ingested + const backendVersionId = decodeVID(destDataGetInfoArr[0].dataStoreVersionId); + if (!(backendVersionId instanceof Error)) { + options.versionId = backendVersionId; // eslint-disable-line no-param-reassign + } } - } + // eslint-disable-next-line + storeMetadataParams.versionId = options.versionId; + // eslint-disable-next-line + storeMetadataParams.versioning = options.versioning; + // eslint-disable-next-line + storeMetadataParams.isNull = options.isNull; + if (options.extraMD) { + Object.assign(storeMetadataParams, options.extraMD); + } + const dataToDelete = options.dataToDelete; + return next( + null, + storeMetadataParams, + destDataGetInfoArr, + destObjMD, + serverSideEncryption, + destBucketMD, + dataToDelete, + ); + }, + ); + }, + function storeNewMetadata( + storeMetadataParams, + destDataGetInfoArr, + destObjMD, + serverSideEncryption, + destBucketMD, + dataToDelete, + next, + ) { + if (destObjMD && destObjMD.uploadId) { // eslint-disable-next-line - storeMetadataParams.versionId = options.versionId; - // eslint-disable-next-line - storeMetadataParams.versioning = options.versioning; - // eslint-disable-next-line - storeMetadataParams.isNull = options.isNull; - if (options.extraMD) { - Object.assign(storeMetadataParams, options.extraMD); - } - const dataToDelete = options.dataToDelete; - return next(null, storeMetadataParams, destDataGetInfoArr, - destObjMD, serverSideEncryption, destBucketMD, - dataToDelete); - }); - }, - function storeNewMetadata(storeMetadataParams, destDataGetInfoArr, - destObjMD, serverSideEncryption, destBucketMD, dataToDelete, next) { - if (destObjMD && destObjMD.uploadId) { - // eslint-disable-next-line - storeMetadataParams.oldReplayId = destObjMD.uploadId; - } + storeMetadataParams.oldReplayId = destObjMD.uploadId; + } - return services.metadataStoreObject(destBucketName, - destDataGetInfoArr, serverSideEncryption, - storeMetadataParams, (err, result) => { - if (err) { - log.debug('error storing new metadata', { error: err }); - return next(err, null, destBucketMD); - } - const sourceObjSize = storeMetadataParams.size; - const destObjPrevSize = (destObjMD && - destObjMD['content-length'] !== undefined) ? - destObjMD['content-length'] : null; - - setExpirationHeaders(responseHeaders, { - lifecycleConfig: destBucketMD.getLifecycleConfiguration(), - objectParams: { - key: destObjectKey, - date: result.lastModified, - tags: result.tags, - }, - }); + return services.metadataStoreObject( + destBucketName, + destDataGetInfoArr, + serverSideEncryption, + storeMetadataParams, + (err, result) => { + if (err) { + log.debug('error storing new metadata', { error: err }); + return next(err, null, destBucketMD); + } + const sourceObjSize = storeMetadataParams.size; + const destObjPrevSize = + destObjMD && destObjMD['content-length'] !== undefined ? destObjMD['content-length'] : null; + + setExpirationHeaders(responseHeaders, { + lifecycleConfig: destBucketMD.getLifecycleConfiguration(), + objectParams: { + key: destObjectKey, + date: result.lastModified, + tags: result.tags, + }, + }); - return next(null, dataToDelete, result, destBucketMD, - storeMetadataParams, serverSideEncryption, - sourceObjSize, destObjPrevSize); - }); - }, - function deleteExistingData(dataToDelete, storingNewMdResult, - destBucketMD, storeMetadataParams, serverSideEncryption, - sourceObjSize, destObjPrevSize, next) { - // Clean up any potential orphans in data if object - // put is an overwrite of already existing - // object with same name, so long as the source is not - // the same as the destination - if (!sourceIsDestination && dataToDelete) { - const newDataStoreName = storeMetadataParams.dataStoreName; - return data.batchDelete(dataToDelete, request.method, - newDataStoreName, log, err => { + return next( + null, + dataToDelete, + result, + destBucketMD, + storeMetadataParams, + serverSideEncryption, + sourceObjSize, + destObjPrevSize, + ); + }, + ); + }, + function deleteExistingData( + dataToDelete, + storingNewMdResult, + destBucketMD, + storeMetadataParams, + serverSideEncryption, + sourceObjSize, + destObjPrevSize, + next, + ) { + // Clean up any potential orphans in data if object + // put is an overwrite of already existing + // object with same name, so long as the source is not + // the same as the destination + if (!sourceIsDestination && dataToDelete) { + const newDataStoreName = storeMetadataParams.dataStoreName; + return data.batchDelete(dataToDelete, request.method, newDataStoreName, log, err => { if (err) { // if error, log the error and move on as it is not // relevant to the client as the client's // object already succeeded putting data, metadata - log.error('error deleting existing data', - { error: err }); + log.error('error deleting existing data', { error: err }); } - next(null, - storingNewMdResult, destBucketMD, storeMetadataParams, - serverSideEncryption, sourceObjSize, destObjPrevSize); + next( + null, + storingNewMdResult, + destBucketMD, + storeMetadataParams, + serverSideEncryption, + sourceObjSize, + destObjPrevSize, + ); }); + } + return next( + null, + storingNewMdResult, + destBucketMD, + storeMetadataParams, + serverSideEncryption, + sourceObjSize, + destObjPrevSize, + ); + }, + ], + ( + err, + storingNewMdResult, + destBucketMD, + storeMetadataParams, + serverSideEncryption, + sourceObjSize, + destObjPrevSize, + ) => { + const corsHeaders = collectCorsHeaders(request.headers.origin, request.method, destBucketMD); + + // Store full object size for server access logs + if (request.serverAccessLog) { + // eslint-disable-next-line no-param-reassign + request.serverAccessLog.objectSize = sourceObjSize; } - return next(null, - storingNewMdResult, destBucketMD, storeMetadataParams, - serverSideEncryption, sourceObjSize, destObjPrevSize); - }, - ], (err, storingNewMdResult, destBucketMD, storeMetadataParams, - serverSideEncryption, sourceObjSize, destObjPrevSize) => { - const corsHeaders = collectCorsHeaders(request.headers.origin, - request.method, destBucketMD); - - // Store full object size for server access logs - if (request.serverAccessLog) { - // eslint-disable-next-line no-param-reassign - request.serverAccessLog.objectSize = sourceObjSize; - } - - // Initialize the queue for internal log request logging - initializeInternalLogRequestQueue(request); - // Queue the source-side access log (REST.COPY.OBJECT_GET) - queueInternalLogRequest(request, { - operation: 'REST.COPY.OBJECT_GET', - sourceBucket, - sourceObject, - objectSize: sourceObjSize || null, - }); - if (err) { + // Initialize the queue for internal log request logging + initializeInternalLogRequestQueue(request); + // Queue the source-side access log (REST.COPY.OBJECT_GET) + queueInternalLogRequest(request, { + operation: 'REST.COPY.OBJECT_GET', + sourceBucket, + sourceObject, + objectSize: sourceObjSize || null, + }); + + if (err) { + monitoring.promMetrics('PUT', destBucketName, err.code, 'copyObject'); + return callback(err, null, corsHeaders); + } + const xml = [ + '', + '', + '', + new Date(storeMetadataParams.lastModifiedDate).toISOString(), + '', + '"', + storeMetadataParams.contentMD5, + '"', + '', + ].join(''); + const additionalHeaders = corsHeaders || {}; + if (serverSideEncryption) { + setSSEHeaders( + additionalHeaders, + serverSideEncryption.algorithm, + serverSideEncryption.configuredMasterKeyId || serverSideEncryption.masterKeyId, + ); + } + if (sourceVersionId) { + additionalHeaders['x-amz-copy-source-version-id'] = versionIdUtils.encode(sourceVersionId); + } + const isVersioned = storingNewMdResult && storingNewMdResult.versionId; + if (isVersioned) { + additionalHeaders['x-amz-version-id'] = versionIdUtils.encode(storingNewMdResult.versionId); + } + + Object.assign(responseHeaders, additionalHeaders); + + // Only pre-existing non-versioned objects get 0 all others use 1 + const numberOfObjects = !isVersioned && destObjPrevSize !== null ? 0 : 1; + + pushMetric('copyObject', log, { + authInfo, + canonicalID: destBucketMD.getOwner(), + bucket: destBucketName, + keys: [destObjectKey], + newByteLength: sourceObjSize, + oldByteLength: isVersioned ? null : destObjPrevSize, + location: storeMetadataParams.dataStoreName, + versionId: isVersioned ? storingNewMdResult.versionId : undefined, + numberOfObjects, + }); monitoring.promMetrics( - 'PUT', destBucketName, err.code, 'copyObject'); - return callback(err, null, corsHeaders); - } - const xml = [ - '', - '', - '', new Date(storeMetadataParams.lastModifiedDate) - .toISOString(), '', - '"', storeMetadataParams.contentMD5, '"', - '', - ].join(''); - const additionalHeaders = corsHeaders || {}; - if (serverSideEncryption) { - setSSEHeaders(additionalHeaders, - serverSideEncryption.algorithm, - serverSideEncryption.configuredMasterKeyId || serverSideEncryption.masterKeyId + 'PUT', + destBucketName, + '200', + 'copyObject', + sourceObjSize, + destObjPrevSize, + isVersioned, ); - } - if (sourceVersionId) { - additionalHeaders['x-amz-copy-source-version-id'] = - versionIdUtils.encode(sourceVersionId); - } - const isVersioned = storingNewMdResult && storingNewMdResult.versionId; - if (isVersioned) { - additionalHeaders['x-amz-version-id'] = - versionIdUtils.encode(storingNewMdResult.versionId); - } - - Object.assign(responseHeaders, additionalHeaders); - - // Only pre-existing non-versioned objects get 0 all others use 1 - const numberOfObjects = !isVersioned && destObjPrevSize !== null ? 0 : 1; - - pushMetric('copyObject', log, { - authInfo, - canonicalID: destBucketMD.getOwner(), - bucket: destBucketName, - keys: [destObjectKey], - newByteLength: sourceObjSize, - oldByteLength: isVersioned ? null : destObjPrevSize, - location: storeMetadataParams.dataStoreName, - versionId: isVersioned ? storingNewMdResult.versionId : undefined, - numberOfObjects, - }); - monitoring.promMetrics('PUT', destBucketName, '200', - 'copyObject', sourceObjSize, destObjPrevSize, isVersioned); - // Add expiration header if lifecycle enabled - return callback(null, xml, responseHeaders); - }); + // Add expiration header if lifecycle enabled + return callback(null, xml, responseHeaders); + }, + ); } module.exports = objectCopy; diff --git a/tests/unit/api/objectCopy.js b/tests/unit/api/objectCopy.js index 2448f3276f..526bef3fb8 100644 --- a/tests/unit/api/objectCopy.js +++ b/tests/unit/api/objectCopy.js @@ -9,18 +9,16 @@ const bucketPutPolicy = require('../../../lib/api/bucketPutPolicy'); const objectPut = require('../../../lib/api/objectPut'); const objectCopy = require('../../../lib/api/objectCopy'); const DummyRequest = require('../DummyRequest'); -const { cleanup, DummyRequestLogger, makeAuthInfo, versioningTestUtils } - = require('../helpers'); +const { cleanup, DummyRequestLogger, makeAuthInfo, versioningTestUtils } = require('../helpers'); const mpuUtils = require('../utils/mpuUtils'); const metadata = require('../metadataswitch'); const { data } = require('../../../lib/data/wrapper'); -const { objectLocationConstraintHeader } = require('../../../constants'); +const constants = require('../../../constants'); +const { objectLocationConstraintHeader } = constants; const { fakeMetadataArchive } = require('../../functional/aws-node-sdk/test/utils/init'); const { config } = require('../../../lib/Config'); -const { - LOCATION_NAME_CRR, -} = require('../../constants'); +const { LOCATION_NAME_CRR } = require('../../constants'); const any = sinon.match.any; @@ -58,50 +56,41 @@ function _createObjectCopyRequest(destBucketName, headers = {}) { const putDestBucketRequest = _createBucketPutRequest(destBucketName); const putSourceBucketRequest = _createBucketPutRequest(sourceBucketName); -const enableVersioningRequest = versioningTestUtils - .createBucketPutVersioningReq(destBucketName, 'Enabled'); -const suspendVersioningRequest = versioningTestUtils - .createBucketPutVersioningReq(destBucketName, 'Suspended'); -const objData = ['foo0', 'foo1', 'foo2'].map(str => - Buffer.from(str, 'utf8')); - +const enableVersioningRequest = versioningTestUtils.createBucketPutVersioningReq(destBucketName, 'Enabled'); +const suspendVersioningRequest = versioningTestUtils.createBucketPutVersioningReq(destBucketName, 'Suspended'); +const objData = ['foo0', 'foo1', 'foo2'].map(str => Buffer.from(str, 'utf8')); describe('objectCopy with versioning', () => { - const testPutObjectRequests = objData.slice(0, 2).map(data => - versioningTestUtils.createPutObjectRequest(destBucketName, objectKey, - data)); - testPutObjectRequests.push(versioningTestUtils - .createPutObjectRequest(sourceBucketName, objectKey, objData[2])); + const testPutObjectRequests = objData + .slice(0, 2) + .map(data => versioningTestUtils.createPutObjectRequest(destBucketName, objectKey, data)); + testPutObjectRequests.push(versioningTestUtils.createPutObjectRequest(sourceBucketName, objectKey, objData[2])); before(done => { cleanup(); sinon.spy(metadata, 'putObjectMD'); - async.series([ - callback => bucketPut(authInfo, putDestBucketRequest, log, - callback), - callback => bucketPut(authInfo, putSourceBucketRequest, log, - callback), - // putting null version: put obj before versioning configured - // in dest bucket - callback => objectPut(authInfo, testPutObjectRequests[0], - undefined, log, callback), - callback => bucketPutVersioning(authInfo, - enableVersioningRequest, log, callback), - // put another version in dest bucket: - callback => objectPut(authInfo, testPutObjectRequests[1], - undefined, log, callback), - callback => bucketPutVersioning(authInfo, - suspendVersioningRequest, log, callback), - // put source object in source bucket - callback => objectPut(authInfo, testPutObjectRequests[2], - undefined, log, callback), - ], err => { - if (err) { - return done(err); - } - versioningTestUtils.assertDataStoreValues(ds, objData); - return done(); - }); + async.series( + [ + callback => bucketPut(authInfo, putDestBucketRequest, log, callback), + callback => bucketPut(authInfo, putSourceBucketRequest, log, callback), + // putting null version: put obj before versioning configured + // in dest bucket + callback => objectPut(authInfo, testPutObjectRequests[0], undefined, log, callback), + callback => bucketPutVersioning(authInfo, enableVersioningRequest, log, callback), + // put another version in dest bucket: + callback => objectPut(authInfo, testPutObjectRequests[1], undefined, log, callback), + callback => bucketPutVersioning(authInfo, suspendVersioningRequest, log, callback), + // put source object in source bucket + callback => objectPut(authInfo, testPutObjectRequests[2], undefined, log, callback), + ], + err => { + if (err) { + return done(err); + } + versioningTestUtils.assertDataStoreValues(ds, objData); + return done(); + }, + ); }); after(() => { @@ -109,13 +98,14 @@ describe('objectCopy with versioning', () => { cleanup(); }); - it('should delete null version when creating new null version, ' + - 'even when null version is not the latest version', done => { - // will have another copy of last object in datastore after objectCopy - const expectedValues = [undefined, objData[1], objData[2], objData[2]]; - const testObjectCopyRequest = _createObjectCopyRequest(destBucketName); - objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, - undefined, log, err => { + it( + 'should delete null version when creating new null version, ' + + 'even when null version is not the latest version', + done => { + // will have another copy of last object in datastore after objectCopy + const expectedValues = [undefined, objData[1], objData[2], objData[2]]; + const testObjectCopyRequest = _createObjectCopyRequest(destBucketName); + objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, undefined, log, err => { assert.ifError(err, `Unexpected err: ${err}`); sinon.assert.calledWith( metadata.putObjectMD.lastCall, @@ -124,44 +114,42 @@ describe('objectCopy with versioning', () => { sinon.match({ _data: { originOp: 's3:ObjectCreated:Copy' } }), sinon.match.any, sinon.match.any, - sinon.match.any + sinon.match.any, ); setImmediate(() => { - versioningTestUtils - .assertDataStoreValues(ds, expectedValues); + versioningTestUtils.assertDataStoreValues(ds, expectedValues); done(); }); }); - }); + }, + ); it('should not copy object with storage-class header not equal to STANDARD', done => { const testObjectCopyRequest = _createObjectCopyRequest(destBucketName); testObjectCopyRequest.headers['x-amz-storage-class'] = 'COLD'; - objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, - undefined, log, err => { - setImmediate(() => { - assert.strictEqual(err.is.InvalidStorageClass, true); - done(); - }); + objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, undefined, log, err => { + setImmediate(() => { + assert.strictEqual(err.is.InvalidStorageClass, true); + done(); }); + }); }); it('should not set bucketOwnerId if requesting account owns dest bucket', done => { const testObjectCopyRequest = _createObjectCopyRequest(destBucketName); - objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, - undefined, log, err => { - assert.ifError(err); - sinon.assert.calledWith( - metadata.putObjectMD.lastCall, - destBucketName, - objectKey, - sinon.match({ _data: { bucketOwnerId: sinon.match.typeOf('undefined') } }), - sinon.match.any, - sinon.match.any, - sinon.match.any - ); - done(); - }); + objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, undefined, log, err => { + assert.ifError(err); + sinon.assert.calledWith( + metadata.putObjectMD.lastCall, + destBucketName, + objectKey, + sinon.match({ _data: { bucketOwnerId: sinon.match.typeOf('undefined') } }), + sinon.match.any, + sinon.match.any, + sinon.match.any, + ); + done(); + }); }); // TODO: S3C-9965 @@ -184,9 +172,7 @@ describe('objectCopy with versioning', () => { Effect: 'Allow', Principal: { AWS: `arn:aws:iam::${authInfo2.shortid}:root` }, Action: ['s3:GetObject'], - Resource: [ - `arn:aws:s3:::${sourceBucketName}/*`, - ], + Resource: [`arn:aws:s3:::${sourceBucketName}/*`], }, ], }), @@ -205,9 +191,7 @@ describe('objectCopy with versioning', () => { Effect: 'Allow', Principal: { AWS: `arn:aws:iam::${authInfo2.shortid}:root` }, Action: ['s3:PutObject'], - Resource: [ - `arn:aws:s3:::${destBucketName}/*`, - ], + Resource: [`arn:aws:s3:::${destBucketName}/*`], }, ], }), @@ -216,51 +200,47 @@ describe('objectCopy with versioning', () => { assert.ifError(err); bucketPutPolicy(authInfo, testPutDestPolicyRequest, log, err => { assert.ifError(err); - objectCopy(authInfo2, testObjectCopyRequest, sourceBucketName, objectKey, - undefined, log, err => { - sinon.assert.calledWith( - metadata.putObjectMD.lastCall, - destBucketName, - objectKey, - sinon.match({ _data: { bucketOwnerId: authInfo.canonicalID } }), - sinon.match.any, - sinon.match.any, - sinon.match.any - ); - assert.ifError(err); - done(); - }); + objectCopy(authInfo2, testObjectCopyRequest, sourceBucketName, objectKey, undefined, log, err => { + sinon.assert.calledWith( + metadata.putObjectMD.lastCall, + destBucketName, + objectKey, + sinon.match({ _data: { bucketOwnerId: authInfo.canonicalID } }), + sinon.match.any, + sinon.match.any, + sinon.match.any, + ); + assert.ifError(err); + done(); + }); }); }); }); }); describe('non-versioned objectCopy', () => { - const testPutObjectRequest = versioningTestUtils - .createPutObjectRequest(sourceBucketName, objectKey, objData[0]); - const testPutDestObjectRequest = versioningTestUtils - .createPutObjectRequest(destBucketName, objectKey, objData[1]); + const testPutObjectRequest = versioningTestUtils.createPutObjectRequest(sourceBucketName, objectKey, objData[0]); + const testPutDestObjectRequest = versioningTestUtils.createPutObjectRequest(destBucketName, objectKey, objData[1]); before(done => { cleanup(); - sinon.stub(metadata, 'putObjectMD') - .callsFake(originalputObjectMD); - - async.series([ - callback => bucketPut(authInfo, putDestBucketRequest, log, - callback), - callback => bucketPut(authInfo, putSourceBucketRequest, log, - callback), - // put source object in source bucket - callback => objectPut(authInfo, testPutObjectRequest, - undefined, log, callback), - ], err => { - if (err) { - return done(err); - } - versioningTestUtils.assertDataStoreValues(ds, objData.slice(0, 1)); - return done(); - }); + sinon.stub(metadata, 'putObjectMD').callsFake(originalputObjectMD); + + async.series( + [ + callback => bucketPut(authInfo, putDestBucketRequest, log, callback), + callback => bucketPut(authInfo, putSourceBucketRequest, log, callback), + // put source object in source bucket + callback => objectPut(authInfo, testPutObjectRequest, undefined, log, callback), + ], + err => { + if (err) { + return done(err); + } + versioningTestUtils.assertDataStoreValues(ds, objData.slice(0, 1)); + return done(); + }, + ); }); after(() => { @@ -271,91 +251,132 @@ describe('non-versioned objectCopy', () => { const testObjectCopyRequest = _createObjectCopyRequest(destBucketName); it('should not leave orphans in data when overwriting a multipart upload', done => { - mpuUtils.createMPU(namespace, destBucketName, objectKey, log, - (err, testUploadId) => { + mpuUtils.createMPU(namespace, destBucketName, objectKey, log, (err, testUploadId) => { assert.ifError(err); - objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, - undefined, log, err => { - assert.ifError(err); - sinon.assert.calledWith(metadata.putObjectMD, - any, any, any, sinon.match({ oldReplayId: testUploadId }), any, any); - done(); - }); + objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, undefined, log, err => { + assert.ifError(err); + sinon.assert.calledWith( + metadata.putObjectMD, + any, + any, + any, + sinon.match({ oldReplayId: testUploadId }), + any, + any, + ); + done(); + }); }); }); it('should not pass needOplogUpdate when creating object', done => { - async.series([ - next => objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, - undefined, log, next), - async () => { - sinon.assert.calledWith(metadata.putObjectMD.lastCall, - destBucketName, objectKey, sinon.match({ - _data: { originOp: 's3:ObjectCreated:Copy' }, - }), sinon.match({ - needOplogUpdate: undefined, - originOp: undefined, - }), any, any); - }, - ], done); + async.series( + [ + next => objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, undefined, log, next), + async () => { + sinon.assert.calledWith( + metadata.putObjectMD.lastCall, + destBucketName, + objectKey, + sinon.match({ + _data: { originOp: 's3:ObjectCreated:Copy' }, + }), + sinon.match({ + needOplogUpdate: undefined, + originOp: undefined, + }), + any, + any, + ); + }, + ], + done, + ); }); it('should not pass needOplogUpdate when replacing object', done => { - async.series([ - next => objectPut(authInfo, testPutDestObjectRequest, undefined, log, next), - next => objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, - undefined, log, next), - async () => { - sinon.assert.calledWith(metadata.putObjectMD.lastCall, - destBucketName, objectKey, sinon.match({ - _data: { originOp: 's3:ObjectCreated:Copy' }, - }), sinon.match({ - needOplogUpdate: undefined, - originOp: undefined, - }), any, any); - }, - ], done); + async.series( + [ + next => objectPut(authInfo, testPutDestObjectRequest, undefined, log, next), + next => objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, undefined, log, next), + async () => { + sinon.assert.calledWith( + metadata.putObjectMD.lastCall, + destBucketName, + objectKey, + sinon.match({ + _data: { originOp: 's3:ObjectCreated:Copy' }, + }), + sinon.match({ + needOplogUpdate: undefined, + originOp: undefined, + }), + any, + any, + ); + }, + ], + done, + ); }); it('should pass needOplogUpdate to metadata when replacing archived object', done => { const archived = { - archiveInfo: { foo: 0, bar: 'stuff' } + archiveInfo: { foo: 0, bar: 'stuff' }, }; - async.series([ - next => objectPut(authInfo, testPutDestObjectRequest, undefined, log, next), - next => fakeMetadataArchive(destBucketName, objectKey, undefined, archived, next), - next => objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, - undefined, log, next), - async () => { - sinon.assert.calledWith(metadata.putObjectMD.lastCall, - destBucketName, objectKey, any, sinon.match({ - needOplogUpdate: true, - originOp: 's3:ReplaceArchivedObject', - }), any, any); - }, - ], done); + async.series( + [ + next => objectPut(authInfo, testPutDestObjectRequest, undefined, log, next), + next => fakeMetadataArchive(destBucketName, objectKey, undefined, archived, next), + next => objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, undefined, log, next), + async () => { + sinon.assert.calledWith( + metadata.putObjectMD.lastCall, + destBucketName, + objectKey, + any, + sinon.match({ + needOplogUpdate: true, + originOp: 's3:ReplaceArchivedObject', + }), + any, + any, + ); + }, + ], + done, + ); }); it('should pass needOplogUpdate to metadata when replacing archived object in version suspended bucket', done => { const archived = { - archiveInfo: { foo: 0, bar: 'stuff' } + archiveInfo: { foo: 0, bar: 'stuff' }, }; - async.series([ - next => bucketPutVersioning(authInfo, suspendVersioningRequest, log, next), - next => objectPut(authInfo, testPutDestObjectRequest, undefined, log, next), - next => fakeMetadataArchive(destBucketName, objectKey, undefined, archived, next), - next => objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, - undefined, log, next), - async () => { - sinon.assert.calledWith(metadata.putObjectMD.lastCall, - destBucketName, objectKey, any, sinon.match({ - needOplogUpdate: true, - originOp: 's3:ReplaceArchivedObject', - }), any, any); - }, - ], done); + async.series( + [ + next => bucketPutVersioning(authInfo, suspendVersioningRequest, log, next), + next => objectPut(authInfo, testPutDestObjectRequest, undefined, log, next), + next => fakeMetadataArchive(destBucketName, objectKey, undefined, archived, next), + next => objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, undefined, log, next), + async () => { + sinon.assert.calledWith( + metadata.putObjectMD.lastCall, + destBucketName, + objectKey, + any, + sinon.match({ + needOplogUpdate: true, + originOp: 's3:ReplaceArchivedObject', + }), + any, + any, + ); + }, + ], + done, + ); }); it('should fail to copy object when setting a crr location as the locationConstraint', done => { @@ -364,14 +385,16 @@ describe('non-versioned objectCopy', () => { [objectLocationConstraintHeader]: LOCATION_NAME_CRR, }); - async.series([ - next => objectPut(authInfo, testPutDestObjectRequest, undefined, log, next), - next => objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, - undefined, log, next), - ], err => { - assert(err.is.InvalidArgument); - done(); - }); + async.series( + [ + next => objectPut(authInfo, testPutDestObjectRequest, undefined, log, next), + next => objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, undefined, log, next), + ], + err => { + assert(err.is.InvalidArgument); + done(); + }, + ); }); }); @@ -379,10 +402,13 @@ describe('objectCopy overheadField', () => { beforeEach(done => { cleanup(); sinon.stub(metadata, 'putObjectMD').callsFake(originalputObjectMD); - async.series([ - next => bucketPut(authInfo, putSourceBucketRequest, log, next), - next => bucketPut(authInfo, putDestBucketRequest, log, next), - ], done); + async.series( + [ + next => bucketPut(authInfo, putSourceBucketRequest, log, next), + next => bucketPut(authInfo, putDestBucketRequest, log, next), + ], + done, + ); }); afterEach(() => { @@ -391,62 +417,82 @@ describe('objectCopy overheadField', () => { }); it('should pass overheadField to metadata.putObjectMD for a non-versioned request', done => { - const testPutObjectRequest = - versioningTestUtils.createPutObjectRequest(sourceBucketName, objectKey, objData[0]); + const testPutObjectRequest = versioningTestUtils.createPutObjectRequest( + sourceBucketName, + objectKey, + objData[0], + ); const testObjectCopyRequest = _createObjectCopyRequest(destBucketName); objectPut(authInfo, testPutObjectRequest, undefined, log, err => { assert.ifError(err); - objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, undefined, log, - err => { - assert.ifError(err); - sinon.assert.calledWith(metadata.putObjectMD.lastCall, - destBucketName, objectKey, any, sinon.match({ overheadField: sinon.match.array }), any, any); - done(); - } - ); + objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, undefined, log, err => { + assert.ifError(err); + sinon.assert.calledWith( + metadata.putObjectMD.lastCall, + destBucketName, + objectKey, + any, + sinon.match({ overheadField: sinon.match.array }), + any, + any, + ); + done(); + }); }); }); it('should pass overheadField to metadata.putObjectMD for a versioned request', done => { - const testPutObjectRequest = - versioningTestUtils.createPutObjectRequest(sourceBucketName, objectKey, objData[0]); + const testPutObjectRequest = versioningTestUtils.createPutObjectRequest( + sourceBucketName, + objectKey, + objData[0], + ); const testObjectCopyRequest = _createObjectCopyRequest(destBucketName); objectPut(authInfo, testPutObjectRequest, undefined, log, err => { assert.ifError(err); bucketPutVersioning(authInfo, enableVersioningRequest, log, err => { assert.ifError(err); - objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, undefined, log, - err => { - assert.ifError(err); - sinon.assert.calledWith(metadata.putObjectMD.lastCall, - destBucketName, objectKey, any, - sinon.match({ overheadField: sinon.match.array }), any, any - ); - done(); - } - ); + objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, undefined, log, err => { + assert.ifError(err); + sinon.assert.calledWith( + metadata.putObjectMD.lastCall, + destBucketName, + objectKey, + any, + sinon.match({ overheadField: sinon.match.array }), + any, + any, + ); + done(); + }); }); }); }); it('should pass overheadField to metadata.putObjectMD for a version-suspended request', done => { - const testPutObjectRequest = - versioningTestUtils.createPutObjectRequest(sourceBucketName, objectKey, objData[0]); + const testPutObjectRequest = versioningTestUtils.createPutObjectRequest( + sourceBucketName, + objectKey, + objData[0], + ); const testObjectCopyRequest = _createObjectCopyRequest(destBucketName); objectPut(authInfo, testPutObjectRequest, undefined, log, err => { assert.ifError(err); bucketPutVersioning(authInfo, suspendVersioningRequest, log, err => { assert.ifError(err); - objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, undefined, log, - err => { - assert.ifError(err); - sinon.assert.calledWith(metadata.putObjectMD.lastCall, - destBucketName, objectKey, any, - sinon.match({ overheadField: sinon.match.array }), any, any - ); - done(); - } - ); + objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, undefined, log, err => { + assert.ifError(err); + sinon.assert.calledWith( + metadata.putObjectMD.lastCall, + destBucketName, + objectKey, + any, + sinon.match({ overheadField: sinon.match.array }), + any, + any, + ); + done(); + }); }); }); }); @@ -461,10 +507,16 @@ describe('objectCopy in ingestion bucket', () => { before(() => { // Setup multi-backend, this is required for ingestion - data.switch(new storage.data.MultipleBackendGateway({ - 'us-east-1': dataClient, - 'us-east-2': dataClient, - }, metadata, data.locStorageCheckFn)); + data.switch( + new storage.data.MultipleBackendGateway( + { + 'us-east-1': dataClient, + 'us-east-2': dataClient, + }, + metadata, + data.locStorageCheckFn, + ), + ); data.implName = 'multipleBackends'; // "mock" the data location, simulating a backend supporting server-side copy @@ -497,19 +549,20 @@ describe('objectCopy in ingestion bucket', () => { sinon.restore(); }); - const newPutIngestBucketRequest = location => new DummyRequest({ - bucketName: destBucketName, - namespace, - headers: { host: `${destBucketName}.s3.amazonaws.com` }, - url: '/', - post: '' + - '' + - `${location}` + - '', - }); - const putSourceObjectRequest = versioningTestUtils.createPutObjectRequest( - sourceBucketName, objectKey, objData[0]); + const newPutIngestBucketRequest = location => + new DummyRequest({ + bucketName: destBucketName, + namespace, + headers: { host: `${destBucketName}.s3.amazonaws.com` }, + url: '/', + post: + '' + + '' + + `${location}` + + '', + }); + const putSourceObjectRequest = versioningTestUtils.createPutObjectRequest(sourceBucketName, objectKey, objData[0]); const newPutObjectRequest = params => { const { location } = params || {}; const r = _createObjectCopyRequest(destBucketName); @@ -526,17 +579,28 @@ describe('objectCopy in ingestion bucket', () => { const versionID = versioning.VersionID.encode(versioning.VersionID.generateVersionId('0', '')); dataClient.copyObject = sinon.stub().yields(null, objectKey, versionID); - async.series([ - next => bucketPut(authInfo, putSourceBucketRequest, log, next), - next => bucketPut(authInfo, newPutIngestBucketRequest('us-east-1:ingest'), log, next), - next => objectPut(authInfo, putSourceObjectRequest, undefined, log, next), - next => objectCopy(authInfo, newPutObjectRequest(), sourceBucketName, objectKey, undefined, log, - (err, xml, headers) => { - assert.ifError(err); - assert.strictEqual(headers['x-amz-version-id'], versionID); - next(); - }), - ], done); + async.series( + [ + next => bucketPut(authInfo, putSourceBucketRequest, log, next), + next => bucketPut(authInfo, newPutIngestBucketRequest('us-east-1:ingest'), log, next), + next => objectPut(authInfo, putSourceObjectRequest, undefined, log, next), + next => + objectCopy( + authInfo, + newPutObjectRequest(), + sourceBucketName, + objectKey, + undefined, + log, + (err, xml, headers) => { + assert.ifError(err); + assert.strictEqual(headers['x-amz-version-id'], versionID); + next(); + }, + ), + ], + done, + ); }); it('should not use the versionID from the backend when writing in another location', done => { @@ -544,34 +608,56 @@ describe('objectCopy in ingestion bucket', () => { dataClient.copyObject = sinon.stub().yields(null, objectKey, versionID); const copyObjectRequest = newPutObjectRequest({ location: 'us-east-2' }); - async.series([ - next => bucketPut(authInfo, putSourceBucketRequest, log, next), - next => bucketPut(authInfo, newPutIngestBucketRequest('us-east-1:ingest'), log, next), - next => objectPut(authInfo, putSourceObjectRequest, undefined, log, next), - next => objectCopy(authInfo, copyObjectRequest, sourceBucketName, objectKey, undefined, log, - (err, xml, headers) => { - assert.ifError(err); - assert.notEqual(headers['x-amz-version-id'], versionID); - next(); - }), - ], done); + async.series( + [ + next => bucketPut(authInfo, putSourceBucketRequest, log, next), + next => bucketPut(authInfo, newPutIngestBucketRequest('us-east-1:ingest'), log, next), + next => objectPut(authInfo, putSourceObjectRequest, undefined, log, next), + next => + objectCopy( + authInfo, + copyObjectRequest, + sourceBucketName, + objectKey, + undefined, + log, + (err, xml, headers) => { + assert.ifError(err); + assert.notEqual(headers['x-amz-version-id'], versionID); + next(); + }, + ), + ], + done, + ); }); it('should not use the versionID from the backend when it is not a valid versionID', done => { const versionID = undefined; dataClient.copyObject = sinon.stub().yields(null, objectKey, versionID); - async.series([ - next => bucketPut(authInfo, putSourceBucketRequest, log, next), - next => bucketPut(authInfo, newPutIngestBucketRequest('us-east-1:ingest'), log, next), - next => objectPut(authInfo, putSourceObjectRequest, undefined, log, next), - next => objectCopy(authInfo, newPutObjectRequest(), sourceBucketName, objectKey, undefined, log, - (err, xml, headers) => { - assert.ifError(err); - assert.notEqual(headers['x-amz-version-id'], versionID); - next(); - }), - ], done); + async.series( + [ + next => bucketPut(authInfo, putSourceBucketRequest, log, next), + next => bucketPut(authInfo, newPutIngestBucketRequest('us-east-1:ingest'), log, next), + next => objectPut(authInfo, putSourceObjectRequest, undefined, log, next), + next => + objectCopy( + authInfo, + newPutObjectRequest(), + sourceBucketName, + objectKey, + undefined, + log, + (err, xml, headers) => { + assert.ifError(err); + assert.notEqual(headers['x-amz-version-id'], versionID); + next(); + }, + ), + ], + done, + ); }); }); @@ -580,12 +666,21 @@ describe('objectCopy with objectKeyByteLimit', () => { beforeEach(done => { cleanup(); - async.series([ - next => bucketPut(authInfo, putDestBucketRequest, log, next), - next => bucketPut(authInfo, putSourceBucketRequest, log, next), - next => objectPut(authInfo, versioningTestUtils.createPutObjectRequest( - sourceBucketName, objectKey, objData[0]), undefined, log, next), - ], done); + async.series( + [ + next => bucketPut(authInfo, putDestBucketRequest, log, next), + next => bucketPut(authInfo, putSourceBucketRequest, log, next), + next => + objectPut( + authInfo, + versioningTestUtils.createPutObjectRequest(sourceBucketName, objectKey, objData[0]), + undefined, + log, + next, + ), + ], + done, + ); }); afterEach(() => { @@ -598,13 +693,12 @@ describe('objectCopy with objectKeyByteLimit', () => { testCopyObjectRequest.objectKey = longDestKey; testCopyObjectRequest.url = `/${destBucketName}/${longDestKey}`; - objectCopy(authInfo, testCopyObjectRequest, sourceBucketName, objectKey, - undefined, log, err => { - assert(err); - assert.strictEqual(err.KeyTooLong, true); - assert.match(err.description, /915/); - done(); - }); + objectCopy(authInfo, testCopyObjectRequest, sourceBucketName, objectKey, undefined, log, err => { + assert(err); + assert.strictEqual(err.KeyTooLong, true); + assert.match(err.description, /915/); + done(); + }); }); it('should accept destination object key longer than 915 bytes with objectKeyByteLimit', done => { @@ -615,12 +709,11 @@ describe('objectCopy with objectKeyByteLimit', () => { testCopyObjectRequest.objectKey = longDestKey; testCopyObjectRequest.url = `/${destBucketName}/${longDestKey}`; - objectCopy(authInfo, testCopyObjectRequest, sourceBucketName, objectKey, - undefined, log, (err, xml) => { - assert.ifError(err); - assert(xml); - done(); - }); + objectCopy(authInfo, testCopyObjectRequest, sourceBucketName, objectKey, undefined, log, (err, xml) => { + assert.ifError(err); + assert(xml); + done(); + }); }); it('should reject destination object key exceeding objectKeyByteLimit', done => { @@ -631,12 +724,69 @@ describe('objectCopy with objectKeyByteLimit', () => { testCopyObjectRequest.objectKey = longDestKey; testCopyObjectRequest.url = `/${destBucketName}/${longDestKey}`; - objectCopy(authInfo, testCopyObjectRequest, sourceBucketName, objectKey, - undefined, log, err => { - assert(err); - assert.strictEqual(err.KeyTooLong, true); - assert.match(err.description, /1024/); - done(); - }); + objectCopy(authInfo, testCopyObjectRequest, sourceBucketName, objectKey, undefined, log, err => { + assert(err); + assert.strictEqual(err.KeyTooLong, true); + assert.match(err.description, /1024/); + done(); + }); + }); +}); + +describe('objectCopy source size limit', () => { + const testPutObjectRequest = versioningTestUtils.createPutObjectRequest(sourceBucketName, objectKey, objData[0]); + const sourceSize = objData[0].length; + let originalMaximumUploadSize; + + before(done => { + cleanup(); + originalMaximumUploadSize = constants.maximumAllowedUploadSize; + async.series( + [ + callback => bucketPut(authInfo, putDestBucketRequest, log, callback), + callback => bucketPut(authInfo, putSourceBucketRequest, log, callback), + callback => objectPut(authInfo, testPutObjectRequest, undefined, log, callback), + ], + done, + ); + }); + + after(() => { + constants.maximumAllowedUploadSize = originalMaximumUploadSize; + config.bypassMaxPutObjectSize = false; + cleanup(); + }); + + it('should allow CopyObject when source size equals the limit', done => { + constants.maximumAllowedUploadSize = sourceSize; + const testObjectCopyRequest = _createObjectCopyRequest(destBucketName); + objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, undefined, log, err => { + assert.ifError(err); + done(); + }); + }); + + it('should reject CopyObject when source size exceeds the limit', done => { + constants.maximumAllowedUploadSize = sourceSize - 1; + const testObjectCopyRequest = _createObjectCopyRequest(destBucketName); + objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, undefined, log, err => { + assert(err); + assert.strictEqual(err.is.InvalidRequest, true); + assert.match( + err.description, + /The specified copy source is larger than the maximum allowable size for a copy source/, + ); + done(); + }); + }); + + it('should allow CopyObject when source size exceeds the limit but bypass flag is set', done => { + constants.maximumAllowedUploadSize = sourceSize - 1; + config.bypassMaxPutObjectSize = true; + const testObjectCopyRequest = _createObjectCopyRequest(destBucketName); + objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, undefined, log, err => { + assert.ifError(err); + done(); + }); }); });