From f50ea3fe3c5187f48eb301dc1f496b561d48e9ec Mon Sep 17 00:00:00 2001 From: Francois Ferrand Date: Mon, 7 Sep 2026 22:15:05 +0200 Subject: [PATCH 1/2] Accept RestoreObject on an object not yet archived An object written directly to a cold location is declared cold as soon as the PUT returns, but its data stays hot until the queue populator drives the transition. Nothing lets a client see that window, so it may legitimately ask for a restore, which was rejected since the object is not in a cold location yet. Such a restore is now accepted and simply recorded in the archive metadata, without any archive info: there is nothing to recall yet, so backbeat initiates it once the archive completes. The client gets the usual 202 and ongoing-request="true", indistinguishable from a slow cold backend, and a repeated request updates the pending one instead of failing as already in progress. The object is still physically hot in that state, so reading it keeps working: the availability check now keys on the archive info rather than on the mere presence of an archive block, which is also how the queue populator tells a direct-to-cold object from a restored one. Restoring it does not reserve any hot space either, as it is already accounted for. Creating such objects remains gated by enableDirectToCold, but restoring one which already exists is not, so that turning the flag off does not strand objects behind. Issue: CLDSRV-956 --- lib/api/apiUtils/object/coldStorage.js | 42 +++++--- tests/unit/api/apiUtils/coldStorage.js | 142 +++++++++++++++++++++++- tests/unit/api/directToCold.js | 144 +++++++++++++++++++++++++ 3 files changed, 313 insertions(+), 15 deletions(-) diff --git a/lib/api/apiUtils/object/coldStorage.js b/lib/api/apiUtils/object/coldStorage.js index dd33022c99..3a4f88441f 100644 --- a/lib/api/apiUtils/object/coldStorage.js +++ b/lib/api/apiUtils/object/coldStorage.js @@ -6,6 +6,7 @@ const { ObjectMDArchive } = require('arsenal').models; const errors = require('arsenal').errors; const errorInstances = require('arsenal').errorInstances; const { config } = require('../../../Config'); +const { isColdStorageClass } = require('./storageClass'); const { locationConstraints } = config; const { scaledMsPerDay } = config.getTimeOptions(); @@ -48,7 +49,9 @@ function setArchiveInfoHeaders(objMD) { } if (objMD.archive) { - headers['x-amz-scal-archive-info'] = JSON.stringify(objMD.archive.archiveInfo); + if (objMD.archive.archiveInfo) { + headers['x-amz-scal-archive-info'] = JSON.stringify(objMD.archive.archiveInfo); + } if (objMD.archive.restoreRequestedAt) { headers['x-amz-scal-restore-requested-at'] = new Date(objMD.archive.restoreRequestedAt).toUTCString(); @@ -96,6 +99,18 @@ function _validateStartRestore(objectMD, log) { // been reset. return undefined; } + if (isColdStorageClass(objectMD['x-amz-storage-class']) && !objectMD.archive?.archiveInfo) { + // The object was written directly to a cold location, and is still awaiting its first + // archive: its data is hot, so there is nothing to recall yet. The request is only + // recorded, and initiated once the archive completes; a repeated request simply updates + // the pending one, instead of being rejected as already in progress. + log.debug('The object is not archived yet, the restore is deferred.', + { + archive: objectMD.archive, + method: '_validateStartRestore', + }); + return undefined; + } const isLocationCold = locationConstraints[objectMD.dataStoreName]?.isCold; if (!isLocationCold) { // return InvalidObjectState error if the object is not in cold storage, @@ -208,19 +223,17 @@ function _updateObjectExpirationDate(objectMD, log) { * */ function _updateRestoreInfo(objectMD, restoreParam, log) { + /* eslint-disable no-param-reassign */ if (!objectMD.archive) { - log.debug('objectMD.archive doesn\'t exits', { - objectMD, - method: '_updateRestoreInfo' - }); - return errorInstances.InternalError.customizeDescription('Archive metadata is missing.'); + // an object awaiting its first archive has no archive metadata yet + objectMD.archive = {}; } - /* eslint-disable no-param-reassign */ objectMD.archive.restoreRequestedAt = new Date(); objectMD.archive.restoreRequestedDays = restoreParam.days; objectMD.originOp = 's3:ObjectRestore:Post'; /* eslint-enable no-param-reassign */ - if (!ObjectMDArchive.isValid(objectMD.archive)) { + // `ObjectMDArchive` requires `archiveInfo`, which is only set once the object is archived + if (objectMD.archive.archiveInfo && !ObjectMDArchive.isValid(objectMD.archive)) { log.debug('archive is not valid', { archive: objectMD.archive, method: '_updateRestoreInfo' @@ -274,12 +287,15 @@ function startRestore(objectMD, restoreParam, log, cb) { * @returns {ArsenalError|null} error if object data is not available */ function verifyColdObjectAvailable(objMD) { + // An object written directly to a cold location has not been archived yet: its data is still + // in the hot location, and stays available even once a restore has been requested for it. + if (!objMD.archive?.archiveInfo) { + return null; + } // return error when object is cold - if (objMD.archive && - // Object is in cold backend - (!objMD.archive.restoreRequestedAt || - // Object is being restored - (objMD.archive.restoreRequestedAt && !objMD.archive.restoreCompletedAt))) { + if (!objMD.archive.restoreRequestedAt || + // Object is being restored + (objMD.archive.restoreRequestedAt && !objMD.archive.restoreCompletedAt)) { const err = errorInstances.InvalidObjectState .customizeDescription('The operation is not valid for the object\'s storage class'); return err; diff --git a/tests/unit/api/apiUtils/coldStorage.js b/tests/unit/api/apiUtils/coldStorage.js index d38a6962b8..b5fe6ac2fc 100644 --- a/tests/unit/api/apiUtils/coldStorage.js +++ b/tests/unit/api/apiUtils/coldStorage.js @@ -2,6 +2,8 @@ const assert = require('assert'); const { errors } = require('arsenal'); const { + getAmzRestoreResHeader, + setArchiveInfoHeaders, startRestore, validatePutVersionId, verifyColdObjectAvailable @@ -17,6 +19,24 @@ const { LOCATION_NAME_DMF, } = require('../../../constants'); +const archiveInfo = { + archiveId: '97a71dfe-49c1-4cca-840a-69199e0b0322', + archiveVersion: 5577006791947779, +}; + +/** + * Object written directly to a cold location: it is declared cold, but its data is still in the + * hot location and it has no archive metadata until the transition completes. + * @returns {object} the object metadata + */ +function directToColdObjectMD() { + return new ObjectMD() + .setDataStoreName('us-east-1') + .setAmzStorageClass(LOCATION_NAME_DMF) + .setTransitionInProgress(true, Date.now()) + .getValue(); +} + describe('cold storage', () => { describe('validatePutVersionId', () => { [ @@ -131,6 +151,75 @@ describe('cold storage', () => { const err = verifyColdObjectAvailable(objectMd.getValue()); assert.ifError(err); }); + + it('should return null if object is awaiting its first archive', () => { + const err = verifyColdObjectAvailable(directToColdObjectMD()); + assert.ifError(err); + }); + + it('should return null if object awaiting its first archive has a pending restore', () => { + const objectMd = directToColdObjectMD(); + objectMd.archive = { + restoreRequestedAt: new Date(), + restoreRequestedDays: 5, + }; + const err = verifyColdObjectAvailable(objectMd); + assert.ifError(err); + }); + + it('should return error if an archived object has no restore request', () => { + const objectMd = new ObjectMD().setDataStoreName(LOCATION_NAME_DMF).getValue(); + objectMd.archive = { + archiveInfo, + restoreCompletedAt: new Date(), + }; + const err = verifyColdObjectAvailable(objectMd); + assert.strictEqual(err.message, 'InvalidObjectState'); + }); + }); + + describe('getAmzRestoreResHeader', () => { + it('should report an ongoing request for an object awaiting its first archive', () => { + const objectMd = directToColdObjectMD(); + objectMd.archive = { + restoreRequestedAt: new Date(), + restoreRequestedDays: 5, + }; + assert.strictEqual(getAmzRestoreResHeader(objectMd), 'ongoing-request="true"'); + }); + + it('should not report anything for an object awaiting its first archive', () => { + assert.strictEqual(getAmzRestoreResHeader(directToColdObjectMD()), undefined); + }); + }); + + describe('setArchiveInfoHeaders', () => { + it('should not set the archive info header when the object is not archived yet', () => { + const restoreRequestedAt = new Date(); + const objectMd = directToColdObjectMD(); + objectMd.archive = { + restoreRequestedAt, + restoreRequestedDays: 5, + }; + + const headers = setArchiveInfoHeaders(objectMd); + assert.strictEqual(headers['x-amz-scal-archive-info'], undefined); + assert.strictEqual(headers['x-amz-scal-restore-requested-at'], restoreRequestedAt.toUTCString()); + assert.strictEqual(headers['x-amz-scal-restore-requested-days'], 5); + assert.strictEqual(headers['x-amz-storage-class'], LOCATION_NAME_DMF); + assert.strictEqual(headers['x-amz-scal-transition-in-progress'], true); + }); + + it('should set the archive info header of an archived object', () => { + const objectMd = new ObjectMD() + .setDataStoreName(LOCATION_NAME_DMF) + .setAmzStorageClass(LOCATION_NAME_DMF) + .setArchive(new ObjectMDArchive(archiveInfo)) + .getValue(); + + const headers = setArchiveInfoHeaders(objectMd); + assert.strictEqual(headers['x-amz-scal-archive-info'], JSON.stringify(archiveInfo)); + }); }); describe('startRestore', () => { @@ -139,6 +228,7 @@ describe('cold storage', () => { startRestore(objectMd, { days: 5 }, log, err => { assert.deepStrictEqual(err, errors.InvalidObjectState); + assert.strictEqual(objectMd.archive, undefined); done(); }); }); @@ -248,15 +338,63 @@ describe('cold storage', () => { }); }); - it('should fail if _updateRestoreInfo fails', done => { + it('should fail if the archive metadata is invalid', done => { const objectMd = new ObjectMD().setDataStoreName( LOCATION_NAME_DMF - ).setArchive(false).getValue(); + ).getValue(); + objectMd.archive = { archiveInfo: 'not an object' }; startRestore(objectMd, { days: 7 }, log, err => { assert.deepStrictEqual(err, errors.InternalError); done(); }); }); + + it('should succeed for an object awaiting its first archive', done => { + const objectMd = directToColdObjectMD(); + + const t = new Date(); + startRestore(objectMd, { days: 7 }, log, (err, isObjectAlreadyRestored) => { + assert.ifError(err); + assert.ok(!isObjectAlreadyRestored); + + // the object has not been archived, so the request is only recorded + assert.strictEqual(objectMd.archive.archiveInfo, undefined); + assert.strictEqual(objectMd.archive.restoreRequestedDays, 7); + assert.ok(objectMd.archive.restoreRequestedAt.getTime() >= t.getTime()); + assert.ok(objectMd.archive.restoreRequestedAt.getTime() <= Date.now()); + assert.strictEqual(objectMd.archive.restoreCompletedAt, undefined); + assert.strictEqual(objectMd.archive.restoreWillExpireAt, undefined); + + // the object is still declared cold, and its data still hot + assert.strictEqual(objectMd['x-amz-storage-class'], LOCATION_NAME_DMF); + assert.strictEqual(objectMd.dataStoreName, 'us-east-1'); + assert.strictEqual(objectMd['x-amz-scal-transition-in-progress'], true); + assert.strictEqual(objectMd.originOp, 's3:ObjectRestore:Post'); + + done(); + }); + }); + + it('should update the pending request of an object awaiting its first archive', done => { + const objectMd = directToColdObjectMD(); + const restoreRequestedAt = new Date(Date.now() - oneDay); + objectMd.archive = { + restoreRequestedAt, + restoreRequestedDays: 5, + }; + + startRestore(objectMd, { days: 9 }, log, (err, isObjectAlreadyRestored) => { + assert.ifError(err); + assert.ok(!isObjectAlreadyRestored); + + assert.strictEqual(objectMd.archive.archiveInfo, undefined); + assert.strictEqual(objectMd.archive.restoreRequestedDays, 9); + assert.ok(objectMd.archive.restoreRequestedAt.getTime() > restoreRequestedAt.getTime()); + assert.strictEqual(objectMd.archive.restoreCompletedAt, undefined); + + done(); + }); + }); }); }); diff --git a/tests/unit/api/directToCold.js b/tests/unit/api/directToCold.js index 4d14e33072..d6e892a717 100644 --- a/tests/unit/api/directToCold.js +++ b/tests/unit/api/directToCold.js @@ -4,6 +4,9 @@ const async = require('async'); const { bucketPut } = require('../../../lib/api/bucketPut'); const objectPut = require('../../../lib/api/objectPut'); const objectCopy = require('../../../lib/api/objectCopy'); +const objectGet = require('../../../lib/api/objectGet'); +const objectHead = require('../../../lib/api/objectHead'); +const objectRestore = require('../../../lib/api/objectRestore'); const initiateMultipartUpload = require('../../../lib/api/initiateMultipartUpload'); const DummyRequest = require('../DummyRequest'); const { cleanup, DummyRequestLogger, makeAuthInfo } = require('../helpers'); @@ -65,6 +68,34 @@ function getObjectMD(key, cb) { return metadata.getObjectMD(bucketName, key, {}, log, cb); } +function getObjectRequest() { + return { + bucketName, + namespace, + objectKey, + headers: { host: `${bucketName}.s3.amazonaws.com` }, + url: `/${bucketName}/${objectKey}`, + actionImplicitDenies: false, + }; +} + +function restoreObjectRequest(days) { + return { + ...getObjectRequest(), + post: + '' + + `${days}` + + 'Standard' + + '', + }; +} + +function putDirectToColdObject(cb) { + return objectPut(authInfo, putObjectRequest({ 'x-amz-storage-class': coldLocation }), undefined, log, err => + cb(err), + ); +} + function assertDirectToCold(md) { assert.strictEqual(md['x-amz-storage-class'], coldLocation); // the data itself stays in the hot location @@ -360,4 +391,117 @@ describe('direct to cold', () => { ); }); }); + + describe('restore during the archive window', () => { + beforeEach(done => { + config.enableDirectToCold = true; + putDirectToColdObject(done); + }); + + it('should accept a restore and record it in the object metadata', done => { + const testStartTime = new Date(); + async.waterfall( + [ + next => + objectRestore(authInfo, restoreObjectRequest(5), log, (err, statusCode) => { + assert.ifError(err); + assert.strictEqual(statusCode, 202); + next(); + }), + next => getObjectMD(objectKey, next), + ], + (err, md) => { + assert.ifError(err); + // the object has not been archived, so the request is only recorded + assert.strictEqual(md.archive.archiveInfo, undefined); + assert.strictEqual(md.archive.restoreRequestedDays, 5); + assert.ok(new Date(md.archive.restoreRequestedAt) >= testStartTime); + assert.strictEqual(md.archive.restoreCompletedAt, undefined); + // the object is still declared cold, its data still hot, and it still needs + // to be transitioned + assert.strictEqual(md['x-amz-storage-class'], coldLocation); + assert.strictEqual(md.dataStoreName, hotLocation); + assert.strictEqual(md['x-amz-scal-transition-in-progress'], true); + assert.strictEqual(md.originOp, 's3:ObjectRestore:Post'); + done(); + }, + ); + }); + + it('should update the pending request on a repeated restore', done => { + async.waterfall( + [ + next => objectRestore(authInfo, restoreObjectRequest(5), log, err => next(err)), + next => getObjectMD(objectKey, next), + (md, next) => + objectRestore(authInfo, restoreObjectRequest(9), log, (err, statusCode) => { + assert.ifError(err); + assert.strictEqual(statusCode, 202); + next(null, md); + }), + (md, next) => getObjectMD(objectKey, (err, updatedMd) => next(err, md, updatedMd)), + ], + (err, md, updatedMd) => { + assert.ifError(err); + assert.strictEqual(updatedMd.archive.restoreRequestedDays, 9); + assert.ok(new Date(updatedMd.archive.restoreRequestedAt) + >= new Date(md.archive.restoreRequestedAt)); + assert.strictEqual(updatedMd.archive.archiveInfo, undefined); + done(); + }, + ); + }); + + it('should report an ongoing restore on HEAD', done => { + async.waterfall( + [ + next => + objectHead(authInfo, getObjectRequest(), log, (err, headers) => { + assert.ifError(err); + // before the restore request, the object simply appears cold + assert.strictEqual(headers['x-amz-storage-class'], coldLocation); + assert.strictEqual(headers['x-amz-meta-scal-s3-transition-in-progress'], true); + assert.strictEqual(headers['x-amz-restore'], undefined); + next(); + }), + next => objectRestore(authInfo, restoreObjectRequest(5), log, err => next(err)), + next => objectHead(authInfo, getObjectRequest(), log, next), + ], + (err, headers) => { + assert.ifError(err); + assert.strictEqual(headers['x-amz-restore'], 'ongoing-request="true"'); + assert.strictEqual(headers['x-amz-storage-class'], coldLocation); + assert.strictEqual(headers['x-amz-meta-scal-s3-transition-in-progress'], true); + done(); + }, + ); + }); + + it('should still allow the object to be read', done => { + async.waterfall( + [ + next => objectRestore(authInfo, restoreObjectRequest(5), log, err => next(err)), + next => objectGet(authInfo, getObjectRequest(), false, log, + (err, _, headers) => next(err, headers)), + ], + (err, headers) => { + // the data is still in the hot location, so it stays readable + assert.ifError(err); + assert.strictEqual(headers['x-amz-restore'], 'ongoing-request="true"'); + done(); + }, + ); + }); + + it('should accept a restore once direct-to-cold is disabled', done => { + // the object was created while the feature was enabled: turning it off must not make + // it unrestorable + config.enableDirectToCold = false; + objectRestore(authInfo, restoreObjectRequest(5), log, (err, statusCode) => { + assert.ifError(err); + assert.strictEqual(statusCode, 202); + done(); + }); + }); + }); }); From e5520113bd26af382aaebd3127c72065dac0d97c Mon Sep 17 00:00:00 2001 From: Francois Ferrand Date: Sat, 12 Sep 2026 19:09:14 +0200 Subject: [PATCH 2/2] Reformat with prettier The prettier CI check runs on the files touched by a PR, so the ones this change touches now need to comply. Issue: CLDSRV-956 --- lib/api/apiUtils/object/coldStorage.js | 86 ++++++------ tests/unit/api/apiUtils/coldStorage.js | 174 ++++++++++++++----------- tests/unit/api/directToCold.js | 9 +- 3 files changed, 145 insertions(+), 124 deletions(-) diff --git a/lib/api/apiUtils/object/coldStorage.js b/lib/api/apiUtils/object/coldStorage.js index 3a4f88441f..d66a957aeb 100644 --- a/lib/api/apiUtils/object/coldStorage.js +++ b/lib/api/apiUtils/object/coldStorage.js @@ -18,9 +18,7 @@ const { scaledMsPerDay } = config.getTimeOptions(); * @returns {string|undefined} x-amz-restore */ function getAmzRestoreResHeader(objMD) { - if (objMD.archive && - objMD.archive.restoreRequestedAt && - !objMD.archive.restoreCompletedAt) { + if (objMD.archive && objMD.archive.restoreRequestedAt && !objMD.archive.restoreCompletedAt) { // Avoid race condition by relying on the `archive` MD of the object // and return the right header after a RESTORE request. // eslint-disable-next-line @@ -86,11 +84,10 @@ function _validateStartRestore(objectMD, log) { if (new Date(objectMD.archive?.restoreWillExpireAt) < new Date(Date.now())) { // return InvalidObjectState error if the restored object is expired // but restore info md of this object has not yet been cleared - log.debug('The restored object already expired.', - { - archive: objectMD.archive, - method: '_validateStartRestore', - }); + log.debug('The restored object already expired.', { + archive: objectMD.archive, + method: '_validateStartRestore', + }); return errors.InvalidObjectState; } @@ -104,32 +101,29 @@ function _validateStartRestore(objectMD, log) { // archive: its data is hot, so there is nothing to recall yet. The request is only // recorded, and initiated once the archive completes; a repeated request simply updates // the pending one, instead of being rejected as already in progress. - log.debug('The object is not archived yet, the restore is deferred.', - { - archive: objectMD.archive, - method: '_validateStartRestore', - }); + log.debug('The object is not archived yet, the restore is deferred.', { + archive: objectMD.archive, + method: '_validateStartRestore', + }); return undefined; } const isLocationCold = locationConstraints[objectMD.dataStoreName]?.isCold; if (!isLocationCold) { // return InvalidObjectState error if the object is not in cold storage, // not in cold storage means either location cold flag not exists or cold flag is explicit false - log.debug('The bucket of the object is not in a cold storage location.', - { - isLocationCold, - method: '_validateStartRestore', - }); + log.debug('The bucket of the object is not in a cold storage location.', { + isLocationCold, + method: '_validateStartRestore', + }); return errors.InvalidObjectState; } if (objectMD.archive?.restoreRequestedAt) { // return RestoreAlreadyInProgress error if the object is currently being restored // check if archive.restoreRequestAt exists and archive.restoreCompletedAt not yet exists - log.debug('The object is currently being restored.', - { - archive: objectMD.archive, - method: '_validateStartRestore', - }); + log.debug('The object is currently being restored.', { + archive: objectMD.archive, + method: '_validateStartRestore', + }); return errors.RestoreAlreadyInProgress; } return undefined; @@ -157,21 +151,24 @@ function validatePutVersionId(objMD, versionId, log) { const isLocationCold = locationConstraints[objMD.dataStoreName]?.isCold; if (!isLocationCold) { - log.error('The object data is not stored in a cold storage location.', - { - isLocationCold, - dataStoreName: objMD.dataStoreName, - method: 'validatePutVersionId', - }); + log.error('The object data is not stored in a cold storage location.', { + isLocationCold, + dataStoreName: objMD.dataStoreName, + method: 'validatePutVersionId', + }); return errors.InvalidObjectState; } // make sure object archive restoration is in progress // NOTE: we do not use putObjectVersion to update the restoration period. - if (!objMD.archive || !objMD.archive.restoreRequestedAt || !objMD.archive.restoreRequestedDays - || objMD.archive.restoreCompletedAt || objMD.archive.restoreWillExpireAt) { - log.error('object archive restoration is not in progress', - { method: 'validatePutVersionId', versionId }); + if ( + !objMD.archive || + !objMD.archive.restoreRequestedAt || + !objMD.archive.restoreRequestedDays || + objMD.archive.restoreCompletedAt || + objMD.archive.restoreWillExpireAt + ) { + log.error('object archive restoration is not in progress', { method: 'validatePutVersionId', versionId }); return errors.InvalidObjectState; } @@ -195,11 +192,11 @@ function _updateObjectExpirationDate(objectMD, log) { const isObjectAlreadyRestored = !!objectMD.archive.restoreCompletedAt; log.debug('The restore status of the object.', { isObjectAlreadyRestored, - method: 'isObjectAlreadyRestored' + method: 'isObjectAlreadyRestored', }); if (isObjectAlreadyRestored) { const expiryDate = new Date(objectMD.archive.restoreRequestedAt); - expiryDate.setTime(expiryDate.getTime() + (objectMD.archive.restoreRequestedDays * scaledMsPerDay)); + expiryDate.setTime(expiryDate.getTime() + objectMD.archive.restoreRequestedDays * scaledMsPerDay); /* eslint-disable no-param-reassign */ objectMD.archive.restoreWillExpireAt = expiryDate; @@ -236,7 +233,7 @@ function _updateRestoreInfo(objectMD, restoreParam, log) { if (objectMD.archive.archiveInfo && !ObjectMDArchive.isValid(objectMD.archive)) { log.debug('archive is not valid', { archive: objectMD.archive, - method: '_updateRestoreInfo' + method: '_updateRestoreInfo', }); return errorInstances.InternalError.customizeDescription('Invalid archive metadata.'); } @@ -262,7 +259,7 @@ function startRestore(objectMD, restoreParam, log, cb) { if (checkResultError) { log.debug('Restore cannot be done.', { error: checkResultError, - method: 'startRestore' + method: 'startRestore', }); return cb(checkResultError); } @@ -270,12 +267,12 @@ function startRestore(objectMD, restoreParam, log, cb) { if (updateResultError) { log.debug('Failed to update restore information.', { error: updateResultError, - method: 'startRestore' + method: 'startRestore', }); return cb(updateResultError); } log.debug('Validated and updated restore information', { - method: 'startRestore' + method: 'startRestore', }); const isObjectAlreadyRestored = _updateObjectExpirationDate(objectMD, log); return cb(null, isObjectAlreadyRestored); @@ -293,11 +290,14 @@ function verifyColdObjectAvailable(objMD) { return null; } // return error when object is cold - if (!objMD.archive.restoreRequestedAt || + if ( + !objMD.archive.restoreRequestedAt || // Object is being restored - (objMD.archive.restoreRequestedAt && !objMD.archive.restoreCompletedAt)) { - const err = errorInstances.InvalidObjectState - .customizeDescription('The operation is not valid for the object\'s storage class'); + (objMD.archive.restoreRequestedAt && !objMD.archive.restoreCompletedAt) + ) { + const err = errorInstances.InvalidObjectState.customizeDescription( + "The operation is not valid for the object's storage class", + ); return err; } return null; diff --git a/tests/unit/api/apiUtils/coldStorage.js b/tests/unit/api/apiUtils/coldStorage.js index b5fe6ac2fc..e1d3b6d9fc 100644 --- a/tests/unit/api/apiUtils/coldStorage.js +++ b/tests/unit/api/apiUtils/coldStorage.js @@ -6,7 +6,7 @@ const { setArchiveInfoHeaders, startRestore, validatePutVersionId, - verifyColdObjectAvailable + verifyColdObjectAvailable, } = require('../../../../lib/api/apiUtils/object/coldStorage'); const { DummyRequestLogger } = require('../../helpers'); const { ObjectMD, ObjectMDArchive } = require('arsenal/build/lib/models'); @@ -15,9 +15,7 @@ const { scaledMsPerDay } = config.getTimeOptions(); const log = new DummyRequestLogger(); const oneDay = 24 * 60 * 60 * 1000; -const { - LOCATION_NAME_DMF, -} = require('../../../constants'); +const { LOCATION_NAME_DMF } = require('../../../constants'); const archiveInfo = { archiveId: '97a71dfe-49c1-4cca-840a-69199e0b0322', @@ -94,29 +92,36 @@ describe('cold storage', () => { }, expectedRes: undefined, }, - ].forEach(testCase => it(testCase.description, () => { - const res = validatePutVersionId(testCase.objMD, testCase.versionId, log); - assert.deepStrictEqual(res, testCase.expectedRes); - })); + ].forEach(testCase => + it(testCase.description, () => { + const res = validatePutVersionId(testCase.objMD, testCase.versionId, log); + assert.deepStrictEqual(res, testCase.expectedRes); + }), + ); }); describe('verifyColdObjectAvailable', () => { [ { description: 'should return error if object is in a cold location', - objectMd: new ObjectMD() - .setArchive(new ObjectMDArchive({ + objectMd: new ObjectMD().setArchive( + new ObjectMDArchive({ archiveId: '97a71dfe-49c1-4cca-840a-69199e0b0322', - archiveVersion: 5577006791947779 - })) + archiveVersion: 5577006791947779, + }), + ), }, { description: 'should return error if object is restoring', - objectMd: new ObjectMD() - .setArchive(new ObjectMDArchive({ - archiveId: '97a71dfe-49c1-4cca-840a-69199e0b0322', - archiveVersion: 5577006791947779, - }, Date.now())) + objectMd: new ObjectMD().setArchive( + new ObjectMDArchive( + { + archiveId: '97a71dfe-49c1-4cca-840a-69199e0b0322', + archiveVersion: 5577006791947779, + }, + Date.now(), + ), + ), }, ].forEach(params => { it(`${params.description}`, () => { @@ -138,16 +143,18 @@ describe('cold storage', () => { }); it('should return null if object is restored', () => { - const objectMd = new ObjectMD().setArchive(new ObjectMDArchive( - { - archiveId: '97a71dfe-49c1-4cca-840a-69199e0b0322', - archiveVersion: 5577006791947779, - }, - /*restoreRequestedAt*/ new Date(0), - /*restoreRequestedDays*/ 5, - /*restoreCompletedAt*/ new Date(1000), - /*restoreWillExpireAt*/ new Date(1000 + 5 * oneDay), - )); + const objectMd = new ObjectMD().setArchive( + new ObjectMDArchive( + { + archiveId: '97a71dfe-49c1-4cca-840a-69199e0b0322', + archiveVersion: 5577006791947779, + }, + /*restoreRequestedAt*/ new Date(0), + /*restoreRequestedDays*/ 5, + /*restoreCompletedAt*/ new Date(1000), + /*restoreWillExpireAt*/ new Date(1000 + 5 * oneDay), + ), + ); const err = verifyColdObjectAvailable(objectMd.getValue()); assert.ifError(err); }); @@ -234,16 +241,19 @@ describe('cold storage', () => { }); it('should fail when object is being restored', done => { - const objectMd = new ObjectMD().setDataStoreName( - LOCATION_NAME_DMF - ).setArchive(new ObjectMDArchive( - { - archiveId: '97a71dfe-49c1-4cca-840a-69199e0b0322', - archiveVersion: 5577006791947779, - }, - /*restoreRequestedAt*/ new Date(0), - /*restoreRequestedDays*/ 5, - )).getValue(); + const objectMd = new ObjectMD() + .setDataStoreName(LOCATION_NAME_DMF) + .setArchive( + new ObjectMDArchive( + { + archiveId: '97a71dfe-49c1-4cca-840a-69199e0b0322', + archiveVersion: 5577006791947779, + }, + /*restoreRequestedAt*/ new Date(0), + /*restoreRequestedDays*/ 5, + ), + ) + .getValue(); startRestore(objectMd, { days: 5 }, log, err => { assert.deepStrictEqual(err, errors.RestoreAlreadyInProgress); @@ -252,18 +262,21 @@ describe('cold storage', () => { }); it('should fail when restored object is expired', done => { - const objectMd = new ObjectMD().setDataStoreName( - LOCATION_NAME_DMF - ).setArchive(new ObjectMDArchive( - { - archiveId: '97a71dfe-49c1-4cca-840a-69199e0b0322', - archiveVersion: 5577006791947779, - }, - /*restoreRequestedAt*/ new Date(0), - /*restoreRequestedDays*/ 5, - /*restoreCompletedAt*/ new Date(Date.now() - 6 * oneDay), - /*restoreWillExpireAt*/ new Date(Date.now() - 1 * oneDay), - )).getValue(); + const objectMd = new ObjectMD() + .setDataStoreName(LOCATION_NAME_DMF) + .setArchive( + new ObjectMDArchive( + { + archiveId: '97a71dfe-49c1-4cca-840a-69199e0b0322', + archiveVersion: 5577006791947779, + }, + /*restoreRequestedAt*/ new Date(0), + /*restoreRequestedDays*/ 5, + /*restoreCompletedAt*/ new Date(Date.now() - 6 * oneDay), + /*restoreWillExpireAt*/ new Date(Date.now() - 1 * oneDay), + ), + ) + .getValue(); startRestore(objectMd, { days: 5 }, log, err => { assert.deepStrictEqual(err, errors.InvalidObjectState); @@ -272,12 +285,15 @@ describe('cold storage', () => { }); it('should succeed for cold object', done => { - const objectMd = new ObjectMD().setDataStoreName( - LOCATION_NAME_DMF - ).setArchive(new ObjectMDArchive({ - archiveId: '97a71dfe-49c1-4cca-840a-69199e0b0322', - archiveVersion: 5577006791947779, - })).getValue(); + const objectMd = new ObjectMD() + .setDataStoreName(LOCATION_NAME_DMF) + .setArchive( + new ObjectMDArchive({ + archiveId: '97a71dfe-49c1-4cca-840a-69199e0b0322', + archiveVersion: 5577006791947779, + }), + ) + .getValue(); const t = new Date(); startRestore(objectMd, { days: 7 }, log, (err, isObjectAlreadyRestored) => { @@ -297,22 +313,26 @@ describe('cold storage', () => { }); it('should succeed for restored object', done => { - const objectMd = new ObjectMD().setDataStoreName( - LOCATION_NAME_DMF - ).setArchive(new ObjectMDArchive( - { - archiveId: '97a71dfe-49c1-4cca-840a-69199e0b0322', - archiveVersion: 5577006791947779, - }, - /*restoreRequestedAt*/ new Date(0), - /*restoreRequestedDays*/ 2, - /*restoreCompletedAt*/ new Date(Date.now() - 1 * oneDay), - /*restoreWillExpireAt*/ new Date(Date.now() + 1 * oneDay), - )).setAmzRestore({ - 'ongoing-request': false, - 'expiry-date': new Date(Date.now() + 1 * oneDay), - 'content-md5': '12345' - }).getValue(); + const objectMd = new ObjectMD() + .setDataStoreName(LOCATION_NAME_DMF) + .setArchive( + new ObjectMDArchive( + { + archiveId: '97a71dfe-49c1-4cca-840a-69199e0b0322', + archiveVersion: 5577006791947779, + }, + /*restoreRequestedAt*/ new Date(0), + /*restoreRequestedDays*/ 2, + /*restoreCompletedAt*/ new Date(Date.now() - 1 * oneDay), + /*restoreWillExpireAt*/ new Date(Date.now() + 1 * oneDay), + ), + ) + .setAmzRestore({ + 'ongoing-request': false, + 'expiry-date': new Date(Date.now() + 1 * oneDay), + 'content-md5': '12345', + }) + .getValue(); const restoreCompletedAt = objectMd.archive.restoreCompletedAt; const t = new Date(); @@ -326,12 +346,14 @@ describe('cold storage', () => { assert.ok(objectMd.archive.restoreRequestedAt.getTime() <= new Date()); assert.strictEqual(objectMd.archive.restoreCompletedAt, restoreCompletedAt); - assert.strictEqual(objectMd.archive.restoreWillExpireAt.getTime(), - objectMd.archive.restoreRequestedAt.getTime() + (5 * scaledMsPerDay)); + assert.strictEqual( + objectMd.archive.restoreWillExpireAt.getTime(), + objectMd.archive.restoreRequestedAt.getTime() + 5 * scaledMsPerDay, + ); assert.deepEqual(objectMd['x-amz-restore'], { 'ongoing-request': false, 'expiry-date': objectMd.archive.restoreWillExpireAt, - 'content-md5': '12345' + 'content-md5': '12345', }); done(); @@ -339,9 +361,7 @@ describe('cold storage', () => { }); it('should fail if the archive metadata is invalid', done => { - const objectMd = new ObjectMD().setDataStoreName( - LOCATION_NAME_DMF - ).getValue(); + const objectMd = new ObjectMD().setDataStoreName(LOCATION_NAME_DMF).getValue(); objectMd.archive = { archiveInfo: 'not an object' }; startRestore(objectMd, { days: 7 }, log, err => { diff --git a/tests/unit/api/directToCold.js b/tests/unit/api/directToCold.js index d6e892a717..483c71f887 100644 --- a/tests/unit/api/directToCold.js +++ b/tests/unit/api/directToCold.js @@ -444,8 +444,9 @@ describe('direct to cold', () => { (err, md, updatedMd) => { assert.ifError(err); assert.strictEqual(updatedMd.archive.restoreRequestedDays, 9); - assert.ok(new Date(updatedMd.archive.restoreRequestedAt) - >= new Date(md.archive.restoreRequestedAt)); + assert.ok( + new Date(updatedMd.archive.restoreRequestedAt) >= new Date(md.archive.restoreRequestedAt), + ); assert.strictEqual(updatedMd.archive.archiveInfo, undefined); done(); }, @@ -481,8 +482,8 @@ describe('direct to cold', () => { async.waterfall( [ next => objectRestore(authInfo, restoreObjectRequest(5), log, err => next(err)), - next => objectGet(authInfo, getObjectRequest(), false, log, - (err, _, headers) => next(err, headers)), + next => + objectGet(authInfo, getObjectRequest(), false, log, (err, _, headers) => next(err, headers)), ], (err, headers) => { // the data is still in the hot location, so it stays readable