diff --git a/docs/API.md b/docs/API.md index 9f587b02..39f5f2e5 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1158,18 +1158,17 @@ console.log(stat) -### removeObject(bucketName, objectName [, removeOpts] [, callback]) +### removeObject(bucketName, objectName [, removeOpts]) Removes an object. **Parameters** -| Param | Type | Description | -| --------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | -| `bucketName` | _string_ | Name of the bucket. | -| `objectName` | _string_ | Name of the object. | -| `removeOpts` | _object_ | Version of the object in the form `{versionId:"my-versionId", governanceBypass: true or false }`. Default is `{}`. (Optional) | -| `callback(err)` | _function_ | Callback function is called with non `null` value in case of error. If no callback is passed, a `Promise` is returned. | +| Param | Type | Description | +| ------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `bucketName` | _string_ | Name of the bucket. | +| `objectName` | _string_ | Name of the object. | +| `removeOpts` | _object_ | Version of the object in the form `{versionId:"my-versionId", governanceBypass: true or false }`. Default is `{}`. (Optional) | **Example 1** @@ -1206,17 +1205,16 @@ Remove an object version locked with retention mode `GOVERNANCE` using the `gove -### removeObjects(bucketName, objectsList[, callback]) +### removeObjects(bucketName, objectsList) Remove all objects in the objectsList. **Parameters** -| Param | Type | Description | -| --------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `bucketName` | _string_ | Name of the bucket. | -| `objectsList` | _object_ | list of objects in the bucket to be removed. any one of the formats: 1. List of Object names as array of strings which are object keys: `['objectname1','objectname2']` 2. List of Object name and VersionId as an object: [{name:"my-obj-name",versionId:"my-versionId"}] | -| `callback(err)` | _function_ | Callback function is called with non `null` value in case of error. | +| Param | Type | Description | +| ------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `bucketName` | _string_ | Name of the bucket. | +| `objectsList` | _object_ | list of objects in the bucket to be removed. any one of the formats: 1. List of Object names as array of strings which are object keys: `['objectname1','objectname2']` 2. List of Object name and VersionId as an object: [{name:"my-obj-name",versionId:"my-versionId"}] | **Example** @@ -1234,13 +1232,8 @@ objectsStream.on('error', function (e) { console.log(e) }) -objectsStream.on('end', function () { - s3Client.removeObjects('my-bucketname', objectsList, function (e) { - if (e) { - return console.log('Unable to remove Objects ', e) - } - console.log('Removed the objects successfully') - }) +objectsStream.on('end', async () => { + await s3Client.removeObjects(bucket, objectsList) }) ``` @@ -1261,13 +1254,8 @@ objectsStream.on('data', function (obj) { objectsStream.on('error', function (e) { return console.log(e) }) -objectsStream.on('end', function () { - s3Client.removeObjects(bucket, objectsList, function (e) { - if (e) { - return console.log(e) - } - console.log('Success') - }) +objectsStream.on('end', async () => { + await s3Client.removeObjects(bucket, objectsList) }) ``` diff --git a/examples/remove-objects.js b/examples/remove-objects.js index c164cb5f..cc38dcd5 100644 --- a/examples/remove-objects.js +++ b/examples/remove-objects.js @@ -21,8 +21,6 @@ import * as Minio from 'minio' const s3Client = new Minio.Client({ endPoint: 's3.amazonaws.com', - port: 9000, - useSSL: false, accessKey: 'YOUR-ACCESSKEYID', secretKey: 'YOUR-SECRETACCESSKEY', }) @@ -48,13 +46,9 @@ function removeObjects(bucketName, prefix, recursive, includeVersion) { return console.log(e) }) - objectsStream.on('end', function () { - s3Client.removeObjects(bucketName, objectsList, function (e) { - if (e) { - return console.log(e) - } - console.log('Success') - }) + objectsStream.on('end', async () => { + const delRes = await s3Client.removeObjects(bucketName, objectsList) + console.log(delRes) }) } @@ -62,18 +56,13 @@ removeObjects(bucketName, prefix, recursive, true) // Versioned objects of a buc removeObjects(bucketName, prefix, recursive, false) // Normal objects of a bucket to be deleted. // Delete Multiple objects and respective versions. -function removeObjectsMultipleVersions() { +async function removeObjectsMultipleVersions() { const deleteList = [ { versionId: '03ed08e1-34ff-4465-91ed-ba50c1e80f39', name: 'prefix-1/out.json.gz' }, { versionId: '35517ae1-18cb-4a21-9551-867f53a10cfe', name: 'dir1/dir2/test.pdf' }, { versionId: '3053f564-9aea-4a59-88f0-7f25d6320a2c', name: 'dir1/dir2/test.pdf' }, ] - s3Client.removeObjects('my-bucket', deleteList, function (e) { - if (e) { - return console.log(e) - } - console.log('Successfully deleted..') - }) + await s3Client.removeObjects('my-bucket', deleteList) } removeObjectsMultipleVersions() diff --git a/src/internal/client.ts b/src/internal/client.ts index 149ae63e..3b6e26fe 100644 --- a/src/internal/client.ts +++ b/src/internal/client.ts @@ -73,6 +73,9 @@ import type { ObjectRetentionInfo, PutObjectLegalHoldOptions, PutTaggingParams, + RemoveObjectsParam, + RemoveObjectsRequestEntry, + RemoveObjectsResponse, RemoveTaggingParams, ReplicationConfig, ReplicationConfigOpts, @@ -90,13 +93,13 @@ import type { VersionIdentificator, } from './type.ts' import type { ListMultipartResult, UploadedPart } from './xml-parser.ts' +import * as xmlParsers from './xml-parser.ts' import { parseCompleteMultipart, parseInitiateMultipart, parseObjectLegalHoldConfig, parseSelectObjectContentResponse, } from './xml-parser.ts' -import * as xmlParsers from './xml-parser.ts' const xml = new xml2js.Builder({ renderOpts: { pretty: false }, headless: true }) @@ -1111,19 +1114,7 @@ export class TypedClient { } } - /** - * Remove the specified object. - * @deprecated use new promise style API - */ - removeObject(bucketName: string, objectName: string, removeOpts: RemoveOptions, callback: NoResultCallback): void - /** - * @deprecated use new promise style API - */ - // @ts-ignore - removeObject(bucketName: string, objectName: string, callback: NoResultCallback): void - async removeObject(bucketName: string, objectName: string, removeOpts?: RemoveOptions): Promise - - async removeObject(bucketName: string, objectName: string, removeOpts: RemoveOptions = {}): Promise { + async removeObject(bucketName: string, objectName: string, removeOpts?: RemoveOptions): Promise { if (!isValidBucketName(bucketName)) { throw new errors.InvalidBucketNameError(`Invalid bucket name: ${bucketName}`) } @@ -1131,22 +1122,22 @@ export class TypedClient { throw new errors.InvalidObjectNameError(`Invalid object name: ${objectName}`) } - if (!isObject(removeOpts)) { + if (removeOpts && !isObject(removeOpts)) { throw new errors.InvalidArgumentError('removeOpts should be of type "object"') } const method = 'DELETE' const headers: RequestHeaders = {} - if (removeOpts.governanceBypass) { + if (removeOpts?.governanceBypass) { headers['X-Amz-Bypass-Governance-Retention'] = true } - if (removeOpts.forceDelete) { + if (removeOpts?.forceDelete) { headers['x-minio-force-delete'] = true } const queryParams: Record = {} - if (removeOpts.versionId) { + if (removeOpts?.versionId) { queryParams.versionId = `${removeOpts.versionId}` } const query = qs.stringify(queryParams) @@ -2429,4 +2420,37 @@ export class TypedClient { const body = await readAsString(res) return xmlParsers.parseObjectRetentionConfig(body) } + + async removeObjects(bucketName: string, objectsList: RemoveObjectsParam): Promise { + if (!isValidBucketName(bucketName)) { + throw new errors.InvalidBucketNameError('Invalid bucket name: ' + bucketName) + } + if (!Array.isArray(objectsList)) { + throw new errors.InvalidArgumentError('objectsList should be a list') + } + + const runDeleteObjects = async (batch: RemoveObjectsParam): Promise => { + const delObjects: RemoveObjectsRequestEntry[] = batch.map((value) => { + return isObject(value) ? { Key: value.name, VersionId: value.versionId } : { Key: value } + }) + + const remObjects = { Delete: { Quiet: true, Object: delObjects } } + const payload = Buffer.from(new xml2js.Builder({ headless: true }).buildObject(remObjects)) + const headers: RequestHeaders = { 'Content-MD5': toMd5(payload) } + + const res = await this.makeRequestAsync({ method: 'POST', bucketName, query: 'delete', headers }, payload) + const body = await readAsString(res) + return xmlParsers.removeObjectsParser(body) + } + + const maxEntries = 1000 // max entries accepted in server for DeleteMultipleObjects API. + // Client side batching + const batches = [] + for (let i = 0; i < objectsList.length; i += maxEntries) { + batches.push(objectsList.slice(i, i + maxEntries)) + } + + const batchResults = await Promise.all(batches.map(runDeleteObjects)) + return batchResults.flat() + } } diff --git a/src/internal/type.ts b/src/internal/type.ts index 1d3c05bf..6bfbe005 100644 --- a/src/internal/type.ts +++ b/src/internal/type.ts @@ -391,3 +391,28 @@ export type ObjectRetentionInfo = { mode: RETENTION_MODES retainUntilDate: string } + +export type RemoveObjectsEntry = { + name: string + versionId?: string +} +export type ObjectName = string + +export type RemoveObjectsParam = ObjectName[] | RemoveObjectsEntry[] + +export type RemoveObjectsRequestEntry = { + Key: string + VersionId?: string +} + +export type RemoveObjectsResponse = + | null + | undefined + | { + Error?: { + Code?: string + Message?: string + Key?: string + VersionId?: string + } + } diff --git a/src/internal/xml-parser.ts b/src/internal/xml-parser.ts index 94c0cc8c..ed71f5a0 100644 --- a/src/internal/xml-parser.ts +++ b/src/internal/xml-parser.ts @@ -557,3 +557,12 @@ export function parseObjectRetentionConfig(xml: string) { retainUntilDate: retentionConfig.RetainUntilDate, } } + +export function removeObjectsParser(xml: string) { + const xmlObj = parseXml(xml) + if (xmlObj.DeleteResult && xmlObj.DeleteResult.Error) { + // return errors as array always. as the response is object in case of single object passed in removeObjects + return toArray(xmlObj.DeleteResult.Error) + } + return [] +} diff --git a/src/minio.d.ts b/src/minio.d.ts index 328261a9..4599a391 100644 --- a/src/minio.d.ts +++ b/src/minio.d.ts @@ -161,9 +161,6 @@ export class Client extends TypedClient { conditions: CopyConditions, ): Promise - removeObjects(bucketName: string, objectsList: string[], callback: NoResultCallback): void - removeObjects(bucketName: string, objectsList: string[]): Promise - removeIncompleteUpload(bucketName: string, objectName: string, callback: NoResultCallback): void removeIncompleteUpload(bucketName: string, objectName: string): Promise composeObject( diff --git a/src/minio.js b/src/minio.js index 869819d8..cc670f18 100644 --- a/src/minio.js +++ b/src/minio.js @@ -19,7 +19,6 @@ import * as Stream from 'node:stream' import async from 'async' import _ from 'lodash' import * as querystring from 'query-string' -import { TextEncoder } from 'web-encoding' import xml2js from 'xml2js' import * as errors from './errors.ts' @@ -47,7 +46,6 @@ import { partsRequired, pipesetup, sanitizeETag, - toMd5, uriEscape, uriResourceEscape, } from './internal/helper.ts' @@ -535,91 +533,6 @@ export class Client extends TypedClient { return readStream } - // Remove all the objects residing in the objectsList. - // - // __Arguments__ - // * `bucketName` _string_: name of the bucket - // * `objectsList` _array_: array of objects of one of the following: - // * List of Object names as array of strings which are object keys: ['objectname1','objectname2'] - // * List of Object name and versionId as an object: [{name:"objectname",versionId:"my-version-id"}] - - removeObjects(bucketName, objectsList, cb) { - if (!isValidBucketName(bucketName)) { - throw new errors.InvalidBucketNameError('Invalid bucket name: ' + bucketName) - } - if (!Array.isArray(objectsList)) { - throw new errors.InvalidArgumentError('objectsList should be a list') - } - if (!isFunction(cb)) { - throw new TypeError('callback should be of type "function"') - } - - const maxEntries = 1000 - const query = 'delete' - const method = 'POST' - - let result = objectsList.reduce( - (result, entry) => { - result.list.push(entry) - if (result.list.length === maxEntries) { - result.listOfList.push(result.list) - result.list = [] - } - return result - }, - { listOfList: [], list: [] }, - ) - - if (result.list.length > 0) { - result.listOfList.push(result.list) - } - - const encoder = new TextEncoder() - const batchResults = [] - - async.eachSeries( - result.listOfList, - (list, batchCb) => { - var objects = [] - list.forEach(function (value) { - if (isObject(value)) { - objects.push({ Key: value.name, VersionId: value.versionId }) - } else { - objects.push({ Key: value }) - } - }) - let deleteObjects = { Delete: { Quiet: true, Object: objects } } - const builder = new xml2js.Builder({ headless: true }) - let payload = builder.buildObject(deleteObjects) - payload = Buffer.from(encoder.encode(payload)) - const headers = {} - - headers['Content-MD5'] = toMd5(payload) - - let removeObjectsResult - this.makeRequest({ method, bucketName, query, headers }, payload, [200], '', true, (e, response) => { - if (e) { - return batchCb(e) - } - pipesetup(response, transformers.removeObjectsTransformer()) - .on('data', (data) => { - removeObjectsResult = data - }) - .on('error', (e) => { - return batchCb(e, null) - }) - .on('end', () => { - batchResults.push(removeObjectsResult) - return batchCb(null, removeObjectsResult) - }) - }) - }, - () => { - cb(null, _.flatten(batchResults)) - }, - ) - } - // Generate a generic presigned URL which can be // used for HTTP methods GET, PUT, HEAD and DELETE // @@ -1101,7 +1014,6 @@ export class Client extends TypedClient { // Promisify various public-facing APIs on the Client module. Client.prototype.copyObject = promisify(Client.prototype.copyObject) -Client.prototype.removeObjects = promisify(Client.prototype.removeObjects) Client.prototype.presignedUrl = promisify(Client.prototype.presignedUrl) Client.prototype.presignedGetObject = promisify(Client.prototype.presignedGetObject) @@ -1153,3 +1065,4 @@ Client.prototype.setBucketEncryption = callbackify(Client.prototype.setBucketEnc Client.prototype.getBucketEncryption = callbackify(Client.prototype.getBucketEncryption) Client.prototype.removeBucketEncryption = callbackify(Client.prototype.removeBucketEncryption) Client.prototype.getObjectRetention = callbackify(Client.prototype.getObjectRetention) +Client.prototype.removeObjects = callbackify(Client.prototype.removeObjects) diff --git a/src/transformers.js b/src/transformers.js index c50f6826..9225650a 100644 --- a/src/transformers.js +++ b/src/transformers.js @@ -127,7 +127,3 @@ export function objectLegalHoldTransformer() { export function uploadPartTransformer() { return getConcater(xmlParsers.uploadPartParser) } - -export function removeObjectsTransformer() { - return getConcater(xmlParsers.removeObjectsParser) -} diff --git a/tests/unit/test.js b/tests/unit/test.js index 98b346d2..846f8c3c 100644 --- a/tests/unit/test.js +++ b/tests/unit/test.js @@ -727,63 +727,68 @@ describe('Client', function () { }) describe('#removeObject(bucket, object, callback)', () => { - it('should fail on null bucket', (done) => { - client.removeObject(null, 'hello', function (err) { - if (err) { - return done() - } - done(new Error('callback should receive error')) - }) + it('should fail on null bucket', async () => { + try { + await client.removeObject(null, 'hello') + } catch (err) { + return + } + throw new Error('callback should receive error') }) - it('should fail on empty bucket', (done) => { - client.removeObject('', 'hello', function (err) { - if (err) { - return done() - } - done(new Error('callback should receive error')) - }) + + it('should fail on empty bucket', async () => { + try { + await client.removeObject('', 'hello') + } catch (err) { + return + } + throw new Error('callback should receive error') }) - it('should fail on empty bucket', (done) => { - client.removeObject(' \n \t ', 'hello', function (err) { - if (err) { - return done() - } - done(new Error('callback should receive error')) - }) + + it('should fail invalid bucket name', async () => { + try { + await client.removeObject(' \n \t ', 'hello') + } catch (err) { + return + } + throw new Error('callback should receive error') }) - it('should fail on null object', (done) => { - client.removeObject('hello', null, function (err) { - if (err) { - return done() - } - done(new Error('callback should receive error')) - }) + + it('should fail on null object', async () => { + try { + await client.removeObject('hello', null) + } catch (err) { + return + } + throw new Error('callback should receive error') }) - it('should fail on empty object', (done) => { - client.removeObject('hello', '', function (err) { - if (err) { - return done() - } - done(new Error('callback should receive error')) - }) + + it('should fail on empty object', async () => { + try { + await client.removeObject('hello', '') + } catch (err) { + return + } + throw new Error('callback should receive error') }) + // Versioning related options as removeOpts - it('should fail on empty (null) removeOpts object', (done) => { - client.removeObject('hello', 'testRemoveOpts', null, function (err) { - if (err) { - return done() - } - done(new Error('callback should receive error')) - }) + it('should fail on empty (null) removeOpts object', async () => { + try { + await client.removeObject('hello', 'testRemoveOpts', null) + } catch (err) { + return + } + throw new Error('callback should receive error') }) - it('should fail on empty (string) removeOpts', (done) => { - client.removeObject('hello', 'testRemoveOpts', '', function (err) { - if (err) { - return done() - } - done(new Error('callback should receive error')) - }) + it('should fail on empty (string) removeOpts', async () => { + try { + await client.removeObject('hello', '', '') + } catch (err) { + return + } + throw new Error('callback should receive error') }) })