diff --git a/docs/API.md b/docs/API.md index a09a505c..e5056a53 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/package-lock.json b/package-lock.json index 52fdc1ed..e3f9ee57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "minio", - "version": "8.0.0", + "version": "8.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "minio", - "version": "8.0.0", + "version": "8.0.1", "license": "Apache-2.0", "dependencies": { "async": "^3.2.4", diff --git a/src/internal/client.ts b/src/internal/client.ts index 5d438a16..bd7a5883 100644 --- a/src/internal/client.ts +++ b/src/internal/client.ts @@ -71,6 +71,9 @@ import type { ObjectMetaData, PutObjectLegalHoldOptions, PutTaggingParams, + RemoveObjectsParam, + RemoveObjectsRequestEntry, + RemoveObjectsResponse, RemoveTaggingParams, ReplicationConfig, ReplicationConfigOpts, @@ -88,13 +91,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 }) @@ -1109,19 +1112,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}`) } @@ -1136,15 +1127,15 @@ export class TypedClient { 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) @@ -2400,4 +2391,37 @@ export class TypedClient { await this.makeRequestAsyncOmit({ method, bucketName, query }, '', [204]) } + + 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 = 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 21b5b086..58b03294 100644 --- a/src/internal/type.ts +++ b/src/internal/type.ts @@ -382,3 +382,28 @@ export type EncryptionRule = { export type EncryptionConfig = { Rule: EncryptionRule[] } + +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 5ce7232c..a6ce8a7c 100644 --- a/src/internal/xml-parser.ts +++ b/src/internal/xml-parser.ts @@ -548,3 +548,12 @@ export function parseLifecycleConfig(xml: string) { export function parseBucketEncryptionConfig(xml: string) { return parseXml(xml) } + +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 c2fa9393..2f798317 100644 --- a/src/minio.d.ts +++ b/src/minio.d.ts @@ -162,9 +162,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 diff --git a/src/minio.js b/src/minio.js index a96ab82f..78d2c7bd 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 // @@ -1139,7 +1052,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) @@ -1191,3 +1103,4 @@ Client.prototype.removeBucketLifecycle = callbackify(Client.prototype.removeBuck Client.prototype.setBucketEncryption = callbackify(Client.prototype.setBucketEncryption) Client.prototype.getBucketEncryption = callbackify(Client.prototype.getBucketEncryption) Client.prototype.removeBucketEncryption = callbackify(Client.prototype.removeBucketEncryption) +Client.prototype.removeObjects = callbackify(Client.prototype.removeObjects) diff --git a/src/transformers.js b/src/transformers.js index 7f79e8b2..1c30b86a 100644 --- a/src/transformers.js +++ b/src/transformers.js @@ -131,7 +131,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 3424dbc3..c4288e77 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') }) })