From 896939611dd967d6ba8a38d7cb8726fa36371e1e Mon Sep 17 00:00:00 2001 From: Prakash Senthil Vel <23444145+prakashsvmx@users.noreply.github.com> Date: Mon, 7 Jun 2021 21:36:34 +0530 Subject: [PATCH] Get/set legal hold APIs (#925) --- README.md | 2 + docs/API.md | 100 ++++++++++++++++-- examples/get-object-legal-hold.js | 54 ++++++++++ examples/set-object-legal-hold.js | 42 ++++++++ src/main/helpers.js | 5 + src/main/minio.js | 133 ++++++++++++++++++++++-- src/main/transformers.js | 4 + src/main/xml-parsers.js | 5 + src/test/functional/functional-tests.js | 116 +++++++++++++++++++++ src/test/unit/test.js | 85 +++++++++++++++ 10 files changed, 527 insertions(+), 19 deletions(-) create mode 100644 examples/get-object-legal-hold.js create mode 100644 examples/set-object-legal-hold.js diff --git a/README.md b/README.md index 1420e2ec..877421c3 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,8 @@ The full API Reference is available here. * [put-object-tagging.js](https://github.com/minio/minio-js/blob/master/examples/put-object-tagging.js) * [get-object-tagging.js](https://github.com/minio/minio-js/blob/master/examples/get-object-tagging.js) * [remove-object-tagging.js](https://github.com/minio/minio-js/blob/master/examples/remove-object-tagging.js) +* [set-object-legal-hold.js](https://github.com/minio/minio-js/blob/master/examples/set-object-legalhold.js) +* [get-object-legal-hold.js](https://github.com/minio/minio-js/blob/master/examples/get-object-legal-hold.js) #### Full Examples : Presigned Operations * [presigned-getobject.js](https://github.com/minio/minio-js/blob/master/examples/presigned-getobject.js) diff --git a/docs/API.md b/docs/API.md index c3505159..a61ba10b 100644 --- a/docs/API.md +++ b/docs/API.md @@ -44,17 +44,18 @@ var s3Client = new Minio.Client({ | [`setBucketLifecycle`](#setBucketLifecycle) | [`putObjectTagging`](#putObjectTagging) | | [`getBucketLifecycle`](#getBucketLifecycle) | [`removeObjectTagging`](#removeObjectTagging) | | [`removeBucketLifecycle`](#removeBucketLifecycle) | [`getObjectTagging`](#getObjectTagging) | -| [`setObjectLockConfig`](#setObjectLockConfig) | [`putObjectRetention`](#putObjectRetention) | -| [`getObjectLockConfig`](#getObjectLockConfig) | [`getObjectRetention`](#getObjectRetention) | -|[`setBucketEncryption`](#setBucketEncryption) | | -|[`getBucketEncryption`](#getBucketEncryption) | | -|[`removeBucketEncryption`](#removeBucketEncryption) | | -| [`removeBucketLifecycle`](#removeBucketLifecycle) | | +| [`setObjectLockConfig`](#setObjectLockConfig) | [`getObjectLegalHold`](#getObjectLegalHold) | +| [`getBucketEncryption`](#getBucketEncryption) | [`setObjectLegalHold`](#setObjectLegalHold) | +| [`getObjectLockConfig`](#getObjectLockConfig) | [`getObjectLegalHold`](#getObjectLegalHold) | +| [`getBucketEncryption`](#getBucketEncryption) | [`setObjectLegalHold`](#setObjectLegalHold) | +| [`setBucketEncryption`](#setBucketEncryption) | | +| [`removeBucketEncryption`](#removeBucketEncryption) | | | [`setBucketReplication`](#setBucketReplication)| | | [`getBucketReplication`](#getBucketReplication)| | | [`removeBucketReplication`](#removeBucketReplication)| | - - +| [`setBucketEncryption`](#setBucketEncryption) | | +| [`getBucketEncryption`](#getBucketEncryption) | | +| [`removeBucketEncryption`](#removeBucketEncryption) | | ## 1. Constructor @@ -1590,6 +1591,89 @@ minioClient.getObjectTagging('bucketname', 'object-name', {versionId:"my-object- }) ``` + +### getObjectLegalHold(bucketName, objectName, getOpts [, callback]) + +Get legal hold on an object. + +__Parameters__ + + +| Param | Type | Description | +|---|---|---| +| `bucketName` |_string_ | Name of the bucket. | +| `objectName` | _string_ | Name of the object. | +| `getOpts` | _object_ | Legal hold configuration options. e.g `{versionId:'my-version-uuid'}` defaults to `{}` . | +| `callback(err)` | _function_ |Callback function is called with non `null` value in case of error. If no callback is passed, a `Promise` is returned. | + + +__Example 1__ + +Get Legal hold of an object. + +```js +minioClient.getObjectLegalHold('bucketName', 'objectName', {}, function(err, res) { + if (err) { + return console.log('Unable to get legal hold config for the object', err.message) + } + console.log('Success', res) +}) +``` + +__Example 2__ + +Get Legal hold of an object with versionId. + +```js +minioClient.getObjectLegalHold('bucketName', 'objectName', { versionId:'my-obj-version-uuid' }, function(err, res) { + if (err) { + return console.log('Unable to get legal hold config for the object', err.message) + } + console.log('Success', res) +}) +``` + + + +### setObjectLegalHold(bucketName, objectName, [,setOpts, callback]) + +Set legal hold on an object. + +__Parameters__ + + +| Param | Type | Description | +|---|---|---| +| `bucketName` |_string_ | Name of the bucket. | +| `objectName` | _string_ | Name of the object. | +| `setOpts` | _object_ | Legal hold configuration options to set. e.g `{versionId:'my-version-uuid', status:'ON or OFF'}` defaults to `{status:'ON'}` if not passed. | +| `callback(err)` | _function_ |Callback function is called with non `null` value in case of error. If no callback is passed, a `Promise` is returned. | + + +__Example 1__ + +Set Legal hold of an object. +```js +minioClient.setObjectLegalHold('bucketName', 'objectName', {Status:"ON"}, function(err, res) { + if (err) { + return console.log('Unable to set legal hold config for the object', err.message) + } + console.log('Success') +}) +``` + +__Example 2__ + +Set Legal hold of an object with versionId. +```js +minioClient.setObjectLegalHold('bucketName', 'objectName', { Status:"ON", versionId:'my-obj-version-uuid' }, function(err, res) { + if (err) { + return console.log('Unable to set legal hold config for the object version', err.message) + } + console.log('Success') +}) +``` + ## 4. Presigned operations Presigned URLs are generated for temporary download/upload access to private objects. diff --git a/examples/get-object-legal-hold.js b/examples/get-object-legal-hold.js new file mode 100644 index 00000000..1e42a14b --- /dev/null +++ b/examples/get-object-legal-hold.js @@ -0,0 +1,54 @@ +/* + * MinIO Javascript Library for Amazon S3 Compatible Cloud Storage, (C) 2021 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY and my-bucketname are +// dummy values, please replace them with original values. + +var Minio = require('minio') + +var s3Client = new Minio.Client({ + endPoint: 's3.amazonaws.com', + accessKey: 'YOUR-ACCESSKEYID', + secretKey: 'YOUR-SECRETACCESSKEY' +}) + +//Get Legalhold config +s3Client.getObjectLegalHold('bucketName', 'objectName', {}, function(err, res) { + if (err) { + return console.log('Unable to get legal hold config for the object', err.message) // Print only the message. + } + console.log(res) +}) + +//With versionId +s3Client.getObjectLegalHold('bucketName', 'objectName', { versionId:'my-obj-version-uuid' }, function(err, res) { + if (err) { + return console.log('Unable to get legal hold config for the object', err.message) // Print only the message. + } + console.log(res) +}) + +//Promise based version: +const objectLegalHoldPromise = s3Client.getObjectLegalHold('bucketName', 'objectName', { versionId:'my-obj-version-uuid' }) +objectLegalHoldPromise.then((data) => { + console.log("Success...", data) +}) + .catch((e)=>{ + // Print only the error message. if called on an object without object lock config. + // e.g: "The specified object does not have a ObjectLock configuration" + console.log(e.message) + + }) diff --git a/examples/set-object-legal-hold.js b/examples/set-object-legal-hold.js new file mode 100644 index 00000000..0320639c --- /dev/null +++ b/examples/set-object-legal-hold.js @@ -0,0 +1,42 @@ +/* + * MinIO Javascript Library for Amazon S3 Compatible Cloud Storage, (C) 2021 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY and my-bucketname are +// dummy values, please replace them with original values. + +var Minio = require('minio') + +var s3Client = new Minio.Client({ + endPoint: 's3.amazonaws.com', + accessKey: 'YOUR-ACCESSKEYID', + secretKey: 'YOUR-SECRETACCESSKEY' +}) + +//Set legal hold config of an object. +s3Client.setObjectLegalHold('bucketName', 'objectName', {status:"ON"}, function(err, res) { + if (err) { + return console.log('Unable to set legal hold config for the object', err) + } + console.log('Success') +}) + +//Set legal hold config of an object with versionId. +s3Client.setObjectLegalHold('bucketName', 'objectName', { status:"ON", versionId:'my-obj-version-uuid' }, function(err, res) { + if (err) { + return console.log('Unable to set legal hold config for the object version', err) + } + console.log('Success') +}) \ No newline at end of file diff --git a/src/main/helpers.js b/src/main/helpers.js index 0bdedc4d..56ae2d4d 100644 --- a/src/main/helpers.js +++ b/src/main/helpers.js @@ -379,3 +379,8 @@ export const RETENTION_VALIDITY_UNITS={ DAYS:"Days", YEARS:"Years" } + +export const LEGAL_HOLD_STATUS={ + ENABLED:"ON", + DISABLED:"OFF" +} \ No newline at end of file diff --git a/src/main/minio.js b/src/main/minio.js index caea391e..6bf1b368 100644 --- a/src/main/minio.js +++ b/src/main/minio.js @@ -36,7 +36,7 @@ import { isString, isObject, isArray, isValidDate, pipesetup, readableStream, isReadableStream, isVirtualHostStyle, insertContentType, makeDateLong, promisify, getVersionId, sanitizeETag, - RETENTION_MODES, RETENTION_VALIDITY_UNITS + RETENTION_MODES, RETENTION_VALIDITY_UNITS, LEGAL_HOLD_STATUS } from './helpers.js' import { signV4, presignSignatureV4, postPresignSignatureV4 } from './signing.js' @@ -2793,22 +2793,23 @@ export class Client { if(!_.isEmpty(encryptionConfig) && encryptionConfig.Rule.length >1){ throw new errors.InvalidArgumentError('Invalid Rule length. Only one rule is allowed.: ' + encryptionConfig.Rule) } - - if (!isFunction(cb)) { + if (cb && !isFunction(cb)) { throw new TypeError('callback should be of type "function"') } - const encryptionObj = _.isEmpty(encryptionConfig) ? { + let encryptionObj =encryptionConfig + if(_.isEmpty(encryptionConfig)) { + encryptionObj={ //Default MinIO Server Supported Rule - Rule:[ - { - ApplyServerSideEncryptionByDefault: { - SSEAlgorithm:"AES256" + Rule:[ + { + ApplyServerSideEncryptionByDefault: { + SSEAlgorithm:"AES256" + } } - } - ] + ] - } : encryptionConfig + }} let method = 'PUT' let query = "encryption" @@ -2935,6 +2936,114 @@ export class Client { this.makeRequest({method, bucketName, query}, '', 200, '', false, cb) } + + getObjectLegalHold(bucketName, objectName, getOpts={}, cb){ + if (!isValidBucketName(bucketName)) { + throw new errors.InvalidBucketNameError('Invalid bucket name: ' + bucketName) + } + if (!isValidObjectName(objectName)) { + throw new errors.InvalidObjectNameError(`Invalid object name: ${objectName}`) + } + + if(isFunction(getOpts)){ + cb= getOpts + getOpts = {} + } + + if (!isObject(getOpts)) { + throw new TypeError('getOpts should be of type "Object"') + } else if(Object.keys(getOpts).length> 0 && getOpts.versionId && !isString((getOpts.versionId))){ + throw new TypeError('versionId should be of type string.:',getOpts.versionId ) + } + + + if (!isFunction(cb)) { + throw new errors.InvalidArgumentError('callback should be of type "function"') + } + + const method = 'GET' + let query = "legal-hold" + + if (getOpts.versionId){ + query +=`&versionId=${getOpts.versionId}` + } + + this.makeRequest({method, bucketName, objectName, query}, '', 200, '', true, (e, response) => { + if (e) return cb(e) + + let legalHoldConfig = Buffer.from('') + pipesetup(response, transformers.objectLegalHoldTransformer()) + .on('data', data => { + legalHoldConfig = data + }) + .on('error', cb) + .on('end', () => { + cb(null, legalHoldConfig) + }) + }) + + } + + setObjectLegalHold(bucketName, objectName, setOpts={}, cb){ + if (!isValidBucketName(bucketName)) { + throw new errors.InvalidBucketNameError('Invalid bucket name: ' + bucketName) + } + if (!isValidObjectName(objectName)) { + throw new errors.InvalidObjectNameError(`Invalid object name: ${objectName}`) + } + + const defaultOpts = { + status:LEGAL_HOLD_STATUS.ENABLED + } + if(isFunction(setOpts)){ + cb= setOpts + setOpts =defaultOpts + } + + if (!isObject(setOpts)) { + throw new TypeError('setOpts should be of type "Object"') + }else { + + if(![LEGAL_HOLD_STATUS.ENABLED, LEGAL_HOLD_STATUS.DISABLED].includes((setOpts.status))){ + throw new TypeError('Invalid status: '+setOpts.status ) + } + if(setOpts.versionId && !setOpts.versionId.length){ + throw new TypeError('versionId should be of type string.:'+ setOpts.versionId ) + } + } + + if (!isFunction(cb)) { + throw new errors.InvalidArgumentError('callback should be of type "function"') + } + + if( _.isEmpty(setOpts)){ + setOpts={ + defaultOpts + } + } + + const method = 'PUT' + let query = "legal-hold" + + if (setOpts.versionId){ + query +=`&versionId=${setOpts.versionId}` + } + + let config={ + Status: setOpts.status + } + + const builder = new xml2js.Builder({rootName:'LegalHold', renderOpts:{'pretty':false}, headless:true}) + const payload = builder.buildObject(config) + const headers = {} + const md5digest = Crypto.createHash('md5').update(payload).digest() + headers['Content-MD5'] = md5digest.toString('base64') + + this.makeRequest({method, bucketName, objectName, query, headers}, payload, 200, '', false, cb) + + + } + get extensions() { if(!this.clientExtensions) { @@ -2991,6 +3100,8 @@ Client.prototype.removeBucketEncryption = promisify(Client.prototype.removeBucke Client.prototype.setBucketReplication =promisify(Client.prototype.setBucketReplication) Client.prototype.getBucketReplication =promisify(Client.prototype.getBucketReplication) Client.prototype.removeBucketReplication=promisify(Client.prototype.removeBucketReplication) +Client.prototype.setObjectLegalHold=promisify(Client.prototype.setObjectLegalHold) +Client.prototype.getObjectLegalHold=promisify(Client.prototype.getObjectLegalHold) export class CopyConditions { constructor() { diff --git a/src/main/transformers.js b/src/main/transformers.js index a84f87d0..8b5b7df6 100644 --- a/src/main/transformers.js +++ b/src/main/transformers.js @@ -240,4 +240,8 @@ export function bucketEncryptionTransformer(){ export function replicationConfigTransformer(){ return getConcater(xmlParsers.parseReplicationConfig) +} + +export function objectLegalHoldTransformer(){ + return getConcater(xmlParsers.parseObjectLegalHoldConfig) } \ No newline at end of file diff --git a/src/main/xml-parsers.js b/src/main/xml-parsers.js index 8734b163..ae303ce2 100644 --- a/src/main/xml-parsers.js +++ b/src/main/xml-parsers.js @@ -536,4 +536,9 @@ export function parseReplicationConfig(xml){ } } return replicationConfig +} + +export function parseObjectLegalHoldConfig(xml){ + const xmlObj = parseXml(xml) + return xmlObj.LegalHold } \ No newline at end of file diff --git a/src/test/functional/functional-tests.js b/src/test/functional/functional-tests.js index e476bbd4..707ed034 100644 --- a/src/test/functional/functional-tests.js +++ b/src/test/functional/functional-tests.js @@ -2374,4 +2374,120 @@ describe('functional tests', function() { //https://docs.min.io/minio/baremetal/replication/replication-overview.html#minio-bucket-replication-clientside }) + describe('Object Legal hold API Tests', ()=>{ + //Isolate the bucket/object for easy debugging and tracking. + //Gateway mode does not support this header. + let versionId = null + describe('Object Legal hold get/set API Test', function () { + const objLegalHoldBucketName = "minio-js-test-legalhold-" + uuid.v4() + const objLegalHoldObjName = "LegalHoldObject" + let isFeatureSupported = false + + + step(`Check if bucket with object lock can be created:_bucketName:${objLegalHoldBucketName}`, done => { + client.makeBucket(objLegalHoldBucketName, {ObjectLocking: true}, (err) => { + if (err && err.code === 'NotImplemented') return done() + isFeatureSupported = true + if (err) return done(err) + done() + }) + }) + + step(`putObject(bucketName, objectName, stream)_bucketName:${objLegalHoldBucketName}, objectName:${objLegalHoldObjName}, stream:100Kib_`, done => { + if (isFeatureSupported) { + client.putObject(objLegalHoldBucketName, objLegalHoldObjName, readableStream(_1byte), _1byte.length, {}) + .then(() => done()) + .catch(done) + } else { + done() + } + }) + + step(`statObject(bucketName, objectName, statOpts)_bucketName:${objLegalHoldBucketName}, objectName:${objLegalHoldObjName}`, done => { + if (isFeatureSupported) { + client.statObject(objLegalHoldBucketName, objLegalHoldObjName, {}, (e, res) => { + versionId = res.versionId + done() + }) + } else { + done() + } + }) + + step(`setObjectLegalHold(bucketName, objectName, setOpts={})_bucketName:${objLegalHoldBucketName}, objectName:${objLegalHoldObjName}`, done => { + if (isFeatureSupported) { + client.setObjectLegalHold(objLegalHoldBucketName, objLegalHoldObjName, () => { + done() + }) + } else { + done() + } + }) + + step(`setObjectLegalHold(bucketName, objectName, setOpts={})_bucketName:${objLegalHoldBucketName}, objectName:${objLegalHoldObjName}`, done => { + if (isFeatureSupported) { + client.setObjectLegalHold(objLegalHoldBucketName, objLegalHoldObjName, {status:"ON", versionId:versionId}, () => { + done() + }) + } else { + done() + } + + }) + + step(`getObjectLegalHold(bucketName, objectName, setOpts={})_bucketName:${objLegalHoldBucketName}, objectName:${objLegalHoldObjName}`, done => { + if (isFeatureSupported) { + client.getObjectLegalHold(objLegalHoldBucketName, objLegalHoldObjName, () => { + done() + }) + } else { + done() + } + }) + + step(`setObjectLegalHold(bucketName, objectName, setOpts={})_bucketName:${objLegalHoldBucketName}, objectName:${objLegalHoldObjName}`, done => { + if (isFeatureSupported) { + client.setObjectLegalHold(objLegalHoldBucketName, objLegalHoldObjName, {status:"OFF", versionId:versionId}, () => { + done() + }) + } else { + done() + } + + }) + + step(`getObjectLegalHold(bucketName, objectName, setOpts={})_bucketName:${objLegalHoldBucketName}, objectName:${objLegalHoldObjName}`, done => { + if (isFeatureSupported) { + client.getObjectLegalHold(objLegalHoldBucketName, objLegalHoldObjName, {versionId:versionId}, () => { + done() + }) + } else { + done() + } + + }) + + step(`removeObject(bucketName, objectName, removeOpts)_bucketName:${objLegalHoldBucketName}, objectName:${objLegalHoldObjName}`, done => { + if(isFeatureSupported) { + client.removeObject(objLegalHoldBucketName, objLegalHoldObjName, {versionId:versionId, governanceBypass:true}, () => { + done() + }) + }else{ + done() + } + + }) + + step(`removeBucket(bucketName, )_bucketName:${objLegalHoldBucketName}`, done => { + if(isFeatureSupported) { + client.removeBucket(objLegalHoldBucketName, () => { + done() + }) + }else{ + done() + } + + }) + + })}) }) diff --git a/src/test/unit/test.js b/src/test/unit/test.js index 92f2f441..1f480041 100644 --- a/src/test/unit/test.js +++ b/src/test/unit/test.js @@ -1342,5 +1342,90 @@ describe('Client', function() { }) }) + + describe('Object Legal Hold APIs', ()=> { + describe('getObjectLegalHold(bucketName, objectName, getOpts={}, cb)', () => { + it('should fail on null bucket', (done) => { + try { + client.getObjectLegalHold(null, function () { + }) + } catch (e) { + done() + } + }) + it('should fail on empty bucket', (done) => { + try { + client.getObjectLegalHold('', function () { + }) + } catch (e) { + done() + } + }) + + it('should fail on null objectName', (done) => { + try { + client.getObjectLegalHold('my-bucket', null, function () { + }) + } catch (e) { + done() + } + }) + it('should fail on null getOpts', (done) => { + try { + client.getObjectLegalHold('my-bucker', 'my-object', null, function () { + }) + } catch (e) { + done() + } + }) + }) + + describe('setObjectLegalHold(bucketName, objectName, setOpts={}, cb)', () => { + it('should fail on null bucket', (done) => { + try { + client.setObjectLegalHold(null, function () { + }) + } catch (e) { + done() + } + }) + it('should fail on empty bucket', (done) => { + try { + client.setObjectLegalHold('', function () { + }) + } catch (e) { + done() + } + }) + + it('should fail on null objectName', (done) => { + try { + client.setObjectLegalHold('my-bucket', null, function () { + }) + } catch (e) { + done() + } + }) + it('should fail on null setOpts', (done) => { + try { + client.setObjectLegalHold('my-bucker', 'my-object', null, function () { + }) + } catch (e) { + done() + } + }) + it('should fail on empty versionId', (done) => { + try { + client.setObjectLegalHold('my-bucker', 'my-object', {}, function () { + }) + } catch (e) { + done() + } + }) + }) + }) + + + })