Compare commits

...

5 Commits

Author SHA1 Message Date
williamlardier a8da1a9290
better coverage and type flexibility 2022-05-02 18:45:00 +02:00
williamlardier 77eb661b7d
fix error message 2022-05-02 17:31:33 +02:00
williamlardier ad3b41f726
fix route 2022-05-02 17:28:01 +02:00
williamlardier 6187054d07
force logical order of fields 2022-05-02 17:26:13 +02:00
williamlardier 1c2709f589
test cold storage changes for arsenal 2022-05-02 16:57:30 +02:00
9 changed files with 454 additions and 1 deletions

View File

@ -37,6 +37,7 @@ const awsSubresources = [
'replication', 'replication',
'versions', 'versions',
'website', 'website',
'restore',
]; ];
export default function getCanonicalizedResource(request: any, clientType: string) { export default function getCanonicalizedResource(request: any, clientType: string) {

View File

@ -4,6 +4,8 @@ const constants = require('../constants');
const VersionIDUtils = require('../versioning/VersionID'); const VersionIDUtils = require('../versioning/VersionID');
const ObjectMDLocation = require('./ObjectMDLocation'); const ObjectMDLocation = require('./ObjectMDLocation');
const ObjectMDAmzRestore = require('./ObjectMDAmzRestore');
const ObjectMDArchive = require('./ObjectMDArchive');
/** /**
* Class to manage metadata object for regular s3 objects (instead of * Class to manage metadata object for regular s3 objects (instead of
@ -101,6 +103,12 @@ class ObjectMD {
// similar to normalizing request but after checkAuth so // similar to normalizing request but after checkAuth so
// string to sign is not impacted. This is GH Issue#89. // string to sign is not impacted. This is GH Issue#89.
'x-amz-storage-class': 'STANDARD', 'x-amz-storage-class': 'STANDARD',
'x-amz-restore': {
'ongoing-request': false,
},
'archive': {
archiveInfo: {},
},
'x-amz-server-side-encryption': '', 'x-amz-server-side-encryption': '',
'x-amz-server-side-encryption-aws-kms-key-id': '', 'x-amz-server-side-encryption-aws-kms-key-id': '',
'x-amz-server-side-encryption-customer-algorithm': '', 'x-amz-server-side-encryption-customer-algorithm': '',
@ -161,7 +169,7 @@ class ObjectMD {
_convertToLatestModel() { _convertToLatestModel() {
// handle backward-compat stuff // handle backward-compat stuff
if (typeof(this._data.location) === 'string') { if (typeof (this._data.location) === 'string') {
this.setLocation([{ key: this._data.location }]); this.setLocation([{ key: this._data.location }]);
} }
} }
@ -1194,6 +1202,56 @@ class ObjectMD {
getValue() { getValue() {
return this._data; return this._data;
} }
/**
* Get x-amz-restore
*
* @returns {ObjectMDAmzRestore} x-amz-restore
*/
getAmzRestore() {
return this._data['x-amz-restore'];
}
/**
* Set x-amz-restore
*
* @param {ObjectMDAmzRestore} value x-amz-restore object
* @returns {ObjectMD} itself
* @throws {Error} case of invalid parameter
*/
setAmzRestore(value) {
// Accept object instance of ObjectMDAmzRestore and Object
if (!(value instanceof ObjectMDAmzRestore) && !ObjectMDAmzRestore.isValid(value)) {
throw new Error('x-amz-restore is must be type of ObjectMDAmzRestore.');
}
this._data['x-amz-restore'] = value;
return this;
}
/**
* Get archive
*
* @returns {ObjectMDArchive} archive
*/
getArchive() {
return this._data.archive;
}
/**
* Set archive
*
* @param {ObjectMDArchive} value archive object
* @returns {ObjectMD} itself
* @throws {Error} case of invalid parameter
*/
setArchive(value) {
// Accept object instance of ObjectMDArchive and Object
if (!(value instanceof ObjectMDArchive) && !ObjectMDArchive.isValid(value)) {
throw new Error('archive is must be type of ObjectMDArchive.');
}
this._data.archive = value;
return this;
}
} }
module.exports = ObjectMD; module.exports = ObjectMD;

View File

@ -0,0 +1,92 @@
// (c) Fujifilm & Scality (TODO)
/**
* class representing the x-amz-restore of object metadata.
*
* @class
*/
class ObjectMDAmzRestore {
/**
*
* @constructor
* @param {boolean} ongoingRequest ongoing-request
* @param {Date} [expiryDate] expiry-date
* @throws {Error} case of invalid parameter
*/
constructor(ongoingRequest, expiryDate = undefined) {
this.setOngoingRequest(ongoingRequest);
this.setExpiryDate(expiryDate);
}
/**
*
* @param {Object} data archiveInfo
* @returns {boolean} true if the provided object is valid
*/
static isValid(data) {
try {
const restoreMD = new ObjectMDAmzRestore(false);
restoreMD.setOngoingRequest(data['ongoing-request']);
restoreMD.setExpiryDate(data['expiry-date']);
return true;
} catch (err) {
return false;
}
}
/**
*
* @returns {boolean} ongoing-request
*/
getOngoingRequest() {
return this['ongoing-request'];
}
/**
*
* @param {boolean} value ongoing-request
* @returns {void}
* @throws {Error} case of invalid parameter
*/
setOngoingRequest(value) {
if (value === undefined) {
throw new Error('ongoing-request is required.');
} else if (typeof value !== 'boolean') {
throw new Error('ongoing-request must be type of boolean.');
}
this['ongoing-request'] = value;
}
/**
*
* @returns {Date} expiry-date
*/
getExpiryDate() {
return this['expiry-date'] || null;
}
/**
*
* @param {Date} value expiry-date
* @returns {void}
* @throws {Error} case of invalid parameter
*/
setExpiryDate(value) {
if (value) {
if (!(value instanceof Date)) {
throw new Error('expiry-date is must be type of Date.');
}
this['expiry-date'] = value;
}
}
/**
*
* @returns {ObjectMDAmzRestore} itself
*/
getValue() {
return this;
}
}
module.exports = ObjectMDAmzRestore;

View File

@ -0,0 +1,169 @@
/**
* class representing the archive of object metadata.
*
* @class
*/
class ObjectMDArchive {
/**
*
* @constructor
* @param {Object} archiveInfo contains the archive info set by the TLP and returned by the TLP jobs
* @param {Date} [restoreRequestedAt] set at the time restore request is made by the client
* @param {Number} [restoreRequestedDays] set at the time restore request is made by the client
* @param {Date} [restoreCompletedAt] set at the time of successful restore
* @param {Date} [restoreWillExpireAt] computed and stored at the time of restore
* @throws {Error} case of invalid parameter
*/
constructor(
archiveInfo,
restoreRequestedAt = undefined,
restoreRequestedDays = undefined,
restoreCompletedAt = undefined,
restoreWillExpireAt = undefined) {
this.setArchiveInfo(archiveInfo);
this.setRestoreRequestedAt(restoreRequestedAt);
this.setRestoreRequestedDays(restoreRequestedDays);
this.setRestoreCompletedAt(restoreCompletedAt);
this.setRestoreWillExpireAt(restoreWillExpireAt);
}
/**
*
* @param {Object} data archiveInfo
* @returns {boolean} true if the provided object is valid
*/
static isValid(data) {
try {
const archiveMD = new ObjectMDArchive({});
archiveMD.setArchiveInfo(data.archiveInfo);
archiveMD.setRestoreRequestedAt(data.restoreRequestedAt);
archiveMD.setRestoreRequestedDays(data.restoreRequestedDays);
archiveMD.setRestoreCompletedAt(data.restoreCompletedAt);
archiveMD.setRestoreWillExpireAt(data.restoreWillExpireAt);
return true;
} catch (err) {
return false;
}
}
/**
*
* @returns {Object} archiveInfo
*/
getArchiveInfo() {
return this.archiveInfo;
}
/**
* @param {Object} value archiveInfo
* @returns {void}
* @throws {Error} case of invalid parameter
*/
setArchiveInfo(value) {
if (!value) {
throw new Error('archiveInfo is required.');
} else if (typeof value !== 'object') {
throw new Error('archiveInfo must be type of object.');
}
this.archiveInfo = value;
}
/**
*
* @returns {Date} restoreRequestedAt
*/
getRestoreRequestedAt() {
return this.restoreRequestedAt;
}
/**
* @param {Object} value restoreRequestedAt
* @returns {void}
* @throws {Error} case of invalid parameter
*/
setRestoreRequestedAt(value) {
if (value) {
if (!(value instanceof Date)) {
throw new Error('restoreRequestedAt must be type of Date.');
}
this.restoreRequestedAt = value;
}
}
/**
*
* @returns {Number} restoreRequestedDays
*/
getRestoreRequestedDays() {
return this.restoreRequestedDays;
}
/**
* @param {Number} value restoreRequestedDays
* @returns {void}
* @throws {Error} case of invalid parameter
*/
setRestoreRequestedDays(value) {
if (value) {
if (isNaN(value)) {
throw new Error('restoreRequestedDays must be type of Number.');
}
this.restoreRequestedDays = value;
}
}
/**
*
* @returns {Date} restoreCompletedAt
*/
getRestoreCompletedAt() {
return this.restoreCompletedAt;
}
/**
* @param {Date} value restoreCompletedAt
* @returns {void}
* @throws {Error} case of invalid parameter
*/
setRestoreCompletedAt(value) {
if (value) {
if (!this.restoreRequestedAt || !this.restoreRequestedDays) {
throw new Error('restoreCompletedAt must be set after restoreRequestedAt and restoreRequestedDays.');
}
if (!(value instanceof Date)) {
throw new Error('restoreCompletedAt must be type of Date.');
}
this.restoreCompletedAt = value;
}
}
/**
*
* @returns {Date} restoreWillExpireAt
*/
getRestoreWillExpireAt() {
return this.restoreWillExpireAt;
}
/**
* @param {Date} value restoreWillExpireAt
* @returns {void}
* @throws {Error} case of invalid parameter
*/
setRestoreWillExpireAt(value) {
if (value) {
if (!this.restoreRequestedAt || !this.restoreRequestedDays) {
throw new Error('restoreWillExpireAt must be set after restoreRequestedAt and restoreRequestedDays.');
}
if (!(value instanceof Date)) {
throw new Error('restoreWillExpireAt must be type of Date.');
}
this.restoreWillExpireAt = value;
}
}
/**
*
* @returns {ObjectMDArchive} itself
*/
getValue() {
return this;
}
}
module.exports = ObjectMDArchive;

View File

@ -47,6 +47,7 @@ const sharedActionMap = {
objectPutLegalHold: 's3:PutObjectLegalHold', objectPutLegalHold: 's3:PutObjectLegalHold',
objectPutRetention: 's3:PutObjectRetention', objectPutRetention: 's3:PutObjectRetention',
objectPutTagging: 's3:PutObjectTagging', objectPutTagging: 's3:PutObjectTagging',
objectRestore: 's3:RestoreObject',
}; };
// action map used for request context // action map used for request context

View File

@ -47,6 +47,14 @@ function routePOST(request, response, api, log) {
corsHeaders)); corsHeaders));
} }
// POST Object restore
if (request.query.restore !== undefined) {
return api.callApiMethod('objectRestore', request, response,
log, (err, statusCode, resHeaders) =>
routesUtils.responseNoBody(err, resHeaders, response,
statusCode, log));
}
return routesUtils.responseNoBody(errors.NotImplemented, null, response, return routesUtils.responseNoBody(errors.NotImplemented, null, response,
200, log); 200, log);
} }

View File

@ -44,6 +44,18 @@ describe('ObjectMD class setters/getters', () => {
['AmzServerVersionId', 'server-version-id'], ['AmzServerVersionId', 'server-version-id'],
['AmzStorageClass', null, 'STANDARD'], ['AmzStorageClass', null, 'STANDARD'],
['AmzStorageClass', 'storage-class'], ['AmzStorageClass', 'storage-class'],
['AmzRestore', null, {
'ongoing-request': false,
}],
['AmzRestore', {
'ongoing-request': false,
}],
['Archive', null, {
archiveInfo: {},
}],
['Archive', {
archiveInfo: {},
}],
['AmzServerSideEncryption', null, ''], ['AmzServerSideEncryption', null, ''],
['AmzServerSideEncryption', 'server-side-encryption'], ['AmzServerSideEncryption', 'server-side-encryption'],
['AmzEncryptionKeyId', null, ''], ['AmzEncryptionKeyId', null, ''],
@ -417,6 +429,8 @@ describe('getAttributes static method', () => {
'x-amz-server-side-encryption-aws-kms-key-id': true, 'x-amz-server-side-encryption-aws-kms-key-id': true,
'x-amz-server-side-encryption-customer-algorithm': true, 'x-amz-server-side-encryption-customer-algorithm': true,
'x-amz-website-redirect-location': true, 'x-amz-website-redirect-location': true,
'x-amz-restore': true,
'archive': true,
'acl': true, 'acl': true,
'key': true, 'key': true,
'location': true, 'location': true,

View File

@ -0,0 +1,26 @@
const assert = require('assert');
const ObjectMDAmzRestore = require('../../../lib/models/ObjectMDAmzRestore');
const amzRestore = new ObjectMDAmzRestore(false, new Date());
describe('ObjectMDAmzRestore value', () => {
it('should return the correct value', () => {
const amzRestoreObj = amzRestore.getValue();
assert.deepStrictEqual(amzRestoreObj, amzRestore);
});
});
describe('ObjectMDAmzRestore setters/getters', () => {
it('should control the ongoing-request attribute', () => {
const ongoing = true;
amzRestore.setOngoingRequest(ongoing);
assert.deepStrictEqual(amzRestore.getOngoingRequest(),
ongoing);
});
it('should control the expiry-date attribute', () => {
const expiry = new Date(100);
amzRestore.setExpiryDate(expiry);
assert.deepStrictEqual(amzRestore.getExpiryDate(),
expiry);
});
});

View File

@ -0,0 +1,84 @@
const assert = require('assert');
const ObjectMDArchive = require('../../../lib/models/ObjectMDArchive');
const testArchive = {
archiveInfo: {
any: 'value',
},
restoreRequestedAt: new Date(0),
restoreRequestedDays: 5,
restoreCompletedAt: new Date(1000),
restoreWillExpireAt: new Date(10000),
};
const archive = new ObjectMDArchive(
testArchive.archiveInfo,
testArchive.restoreRequestedAt,
testArchive.restoreRequestedDays,
testArchive.restoreCompletedAt,
testArchive.restoreWillExpireAt,
);
describe('ObjectMDArchive value', () => {
it('should return the correct value', () => {
const amzRestoreObj = archive.getValue();
assert.deepStrictEqual(amzRestoreObj, archive);
});
});
describe('ObjectMDArchive setters/getters', () => {
let archived = null;
beforeEach(() => {
archived = new ObjectMDArchive(
testArchive.archiveInfo,
undefined,
undefined,
undefined,
undefined,
);
});
it('should control the archiveInfo attribute', () => {
const info = {
test: 'data',
};
archive.setArchiveInfo(info);
assert.deepStrictEqual(archive.getArchiveInfo(),
info);
});
it('should control the restoreRequestedAt attribute', () => {
const requestedAt = new Date(123456);
assert.doesNotThrow(() => {
archived.setRestoreRequestedAt(requestedAt);
});
archive.setRestoreRequestedAt(requestedAt);
assert.deepStrictEqual(archive.getRestoreRequestedAt(),
requestedAt);
});
it('should control the restoreRequestedDays attribute', () => {
const requestedDays = 8;
assert.doesNotThrow(() => {
archived.setRestoreRequestedDays(requestedDays);
});
archive.setRestoreRequestedDays(requestedDays);
assert.deepStrictEqual(archive.getRestoreRequestedDays(),
requestedDays);
});
it('should control the restoreCompletedAt attribute', () => {
const completedAt = new Date(123456);
assert.throws(() => {
archived.setRestoreCompletedAt(completedAt);
});
archive.setRestoreCompletedAt(completedAt);
assert.deepStrictEqual(archive.getRestoreCompletedAt(),
completedAt);
});
it('should control the restoreWillExpireAt attribute', () => {
const willExpireAt = new Date(123456);
assert.throws(() => {
archived.setRestoreWillExpireAt(willExpireAt);
});
archive.setRestoreWillExpireAt(willExpireAt);
assert.deepStrictEqual(archive.getRestoreWillExpireAt(),
willExpireAt);
});
});