diff --git a/docs/API.md b/docs/API.md
index 48787929..53233b1b 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -1089,32 +1089,25 @@ minioClient.fPutObject('mybucket', '40mbfile', file, metaData, function (err, ob
-### copyObject(bucketName, objectName, sourceObject, conditions[, callback])
+### copyObject(targetBucketName, targetObjectName, sourceBucketNameAndObjectName [,conditions])
Copy a source object into a new object in the specified bucket.
**Parameters**
-| Param | Type | Description |
-| ------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `bucketName` | _string_ | Name of the bucket. |
-| `objectName` | _string_ | Name of the object. |
-| `sourceObject` | _string_ | Path of the file to be copied. |
-| `conditions` | _CopyConditions_ | Conditions to be satisfied before allowing object copy. |
-| `callback(err, {etag, lastModified})` | _function_ | Non-null `err` indicates error, `etag` _string_ and lastModified _Date_ are the etag and the last modified date of the object newly copied. If no callback is passed, a `Promise` is returned. |
+| Param | Type | Description |
+| ------------------------------- | ---------------- | ------------------------------------------------------- |
+| `targetBucketName` | _string_ | Name of the bucket. |
+| `targetObjectName` | _string_ | Name of the object. |
+| `sourceBucketNameAndObjectName` | _string_ | Path of the file to be copied. |
+| `conditions` | _CopyConditions_ | Conditions to be satisfied before allowing object copy. |
**Example**
```js
const conds = new Minio.CopyConditions()
conds.setMatchETag('bd891862ea3e22c93ed53a098218791d')
-minioClient.copyObject('mybucket', 'newobject', '/mybucket/srcobject', conds, function (e, data) {
- if (e) {
- return console.log(e)
- }
- console.log('Successfully copied the object:')
- console.log('etag = ' + data.etag + ', lastModified = ' + data.lastModified)
-})
+await minioClient.copyObject('mybucket', 'newobject', '/mybucket/srcobject', conds)
```
diff --git a/examples/copy-object.js b/examples/copy-object.mjs
similarity index 72%
rename from examples/copy-object.js
rename to examples/copy-object.mjs
index 9e0107cc..dd75ae99 100644
--- a/examples/copy-object.js
+++ b/examples/copy-object.mjs
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-// Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY, my-bucketname, my-objectname,
+// Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY, my-target-bucketname, my-target-objectname,
// my-src-bucketname and my-src-objectname are dummy values, please replace
// them with original values.
@@ -29,16 +29,4 @@ const s3Client = new Minio.Client({
const conds = new Minio.CopyConditions()
conds.setMatchETag('bd891862ea3e22c93ed53a098218791d')
-s3Client.copyObject(
- 'my-bucketname',
- 'my-objectname',
- '/my-src-bucketname/my-src-objectname',
- conds,
- function (e, data) {
- if (e) {
- return console.log(e)
- }
- console.log('Successfully copied the object:')
- console.log('etag = ' + data.etag + ', lastModified = ' + data.lastModified)
- },
-)
+await s3Client.copyObject('my-target-bucketname', 'my-target-objectname', '/my-src-bucketname/my-src-objectname', conds)
diff --git a/src/internal/client.ts b/src/internal/client.ts
index e81a0a53..1d6c70a8 100644
--- a/src/internal/client.ts
+++ b/src/internal/client.ts
@@ -1,5 +1,6 @@
import * as crypto from 'node:crypto'
import * as fs from 'node:fs'
+import type { IncomingHttpHeaders } from 'node:http'
import * as http from 'node:http'
import * as https from 'node:https'
import * as path from 'node:path'
@@ -15,13 +16,22 @@ import xml2js from 'xml2js'
import { CredentialProvider } from '../CredentialProvider.ts'
import * as errors from '../errors.ts'
import type { SelectResults } from '../helpers.ts'
-import { DEFAULT_REGION, LEGAL_HOLD_STATUS, RETENTION_MODES, RETENTION_VALIDITY_UNITS } from '../helpers.ts'
+import {
+ CopyDestinationOptions,
+ CopySourceOptions,
+ DEFAULT_REGION,
+ LEGAL_HOLD_STATUS,
+ RETENTION_MODES,
+ RETENTION_VALIDITY_UNITS,
+} from '../helpers.ts'
import { signV4 } from '../signing.ts'
import { fsp, streamPromise } from './async.ts'
+import { CopyConditions } from './copy-conditions.ts'
import { Extensions } from './extensions.ts'
import {
extractMetadata,
getContentLength,
+ getSourceVersionId,
getVersionId,
hashBinary,
insertContentType,
@@ -59,6 +69,9 @@ import type {
BucketItemStat,
BucketStream,
BucketVersioningConfiguration,
+ CopyObjectParams,
+ CopyObjectResult,
+ CopyObjectResultV2,
EncryptionConfig,
GetObjectLegalHoldOptions,
GetObjectRetentionOpts,
@@ -2465,4 +2478,126 @@ export class TypedClient {
const query = `uploadId=${removeUploadId}`
await this.makeRequestAsyncOmit({ method, bucketName, objectName, query }, '', [204])
}
+
+ private async copyObjectV1(
+ targetBucketName: string,
+ targetObjectName: string,
+ sourceBucketNameAndObjectName: string,
+ conditions?: null | CopyConditions,
+ ) {
+ if (typeof conditions == 'function') {
+ conditions = null
+ }
+
+ if (!isValidBucketName(targetBucketName)) {
+ throw new errors.InvalidBucketNameError('Invalid bucket name: ' + targetBucketName)
+ }
+ if (!isValidObjectName(targetObjectName)) {
+ throw new errors.InvalidObjectNameError(`Invalid object name: ${targetObjectName}`)
+ }
+ if (!isString(sourceBucketNameAndObjectName)) {
+ throw new TypeError('sourceBucketNameAndObjectName should be of type "string"')
+ }
+ if (sourceBucketNameAndObjectName === '') {
+ throw new errors.InvalidPrefixError(`Empty source prefix`)
+ }
+
+ if (conditions != null && !(conditions instanceof CopyConditions)) {
+ throw new TypeError('conditions should be of type "CopyConditions"')
+ }
+
+ const headers: RequestHeaders = {}
+ headers['x-amz-copy-source'] = uriResourceEscape(sourceBucketNameAndObjectName)
+
+ if (conditions) {
+ if (conditions.modified !== '') {
+ headers['x-amz-copy-source-if-modified-since'] = conditions.modified
+ }
+ if (conditions.unmodified !== '') {
+ headers['x-amz-copy-source-if-unmodified-since'] = conditions.unmodified
+ }
+ if (conditions.matchETag !== '') {
+ headers['x-amz-copy-source-if-match'] = conditions.matchETag
+ }
+ if (conditions.matchETagExcept !== '') {
+ headers['x-amz-copy-source-if-none-match'] = conditions.matchETagExcept
+ }
+ }
+
+ const method = 'PUT'
+
+ const res = await this.makeRequestAsync({
+ method,
+ bucketName: targetBucketName,
+ objectName: targetObjectName,
+ headers,
+ })
+ const body = await readAsString(res)
+ return xmlParsers.parseCopyObject(body)
+ }
+
+ private async copyObjectV2(
+ sourceConfig: CopySourceOptions,
+ destConfig: CopyDestinationOptions,
+ ): Promise {
+ if (!(sourceConfig instanceof CopySourceOptions)) {
+ throw new errors.InvalidArgumentError('sourceConfig should of type CopySourceOptions ')
+ }
+ if (!(destConfig instanceof CopyDestinationOptions)) {
+ throw new errors.InvalidArgumentError('destConfig should of type CopyDestinationOptions ')
+ }
+ if (!destConfig.validate()) {
+ return Promise.reject()
+ }
+ if (!destConfig.validate()) {
+ return Promise.reject()
+ }
+
+ const headers = Object.assign({}, sourceConfig.getHeaders(), destConfig.getHeaders())
+
+ const bucketName = destConfig.Bucket
+ const objectName = destConfig.Object
+
+ const method = 'PUT'
+
+ const res = await this.makeRequestAsync({ method, bucketName, objectName, headers })
+ const body = await readAsString(res)
+ const copyRes = xmlParsers.parseCopyObject(body)
+ const resHeaders: IncomingHttpHeaders = res.headers
+
+ const sizeHeaderValue = resHeaders && resHeaders['content-length']
+ const size = typeof sizeHeaderValue === 'number' ? sizeHeaderValue : undefined
+
+ return {
+ Bucket: destConfig.Bucket,
+ Key: destConfig.Object,
+ LastModified: copyRes.lastModified,
+ MetaData: extractMetadata(resHeaders as ResponseHeader),
+ VersionId: getVersionId(resHeaders as ResponseHeader),
+ SourceVersionId: getSourceVersionId(resHeaders as ResponseHeader),
+ Etag: sanitizeETag(resHeaders.etag),
+ Size: size,
+ }
+ }
+
+ async copyObject(source: CopySourceOptions, dest: CopyDestinationOptions): Promise
+ async copyObject(
+ targetBucketName: string,
+ targetObjectName: string,
+ sourceBucketNameAndObjectName: string,
+ conditions?: CopyConditions,
+ ): Promise
+ async copyObject(...allArgs: CopyObjectParams): Promise {
+ if (typeof allArgs[0] === 'string') {
+ const [targetBucketName, targetObjectName, sourceBucketNameAndObjectName, conditions] = allArgs as [
+ string,
+ string,
+ string,
+ CopyConditions?,
+ ]
+ return await this.copyObjectV1(targetBucketName, targetObjectName, sourceBucketNameAndObjectName, conditions)
+ }
+ const [source, dest] = allArgs as [CopySourceOptions, CopyDestinationOptions]
+ return await this.copyObjectV2(source, dest)
+ }
}
diff --git a/src/internal/type.ts b/src/internal/type.ts
index 6bfbe005..582ac1dc 100644
--- a/src/internal/type.ts
+++ b/src/internal/type.ts
@@ -1,6 +1,9 @@
import type * as http from 'node:http'
import type { Readable as ReadableStream } from 'node:stream'
+import type { CopyDestinationOptions, CopySourceOptions } from '../helpers.ts'
+import type { CopyConditions } from './copy-conditions.ts'
+
export type VersionIdentificator = {
versionId?: string
}
@@ -416,3 +419,21 @@ export type RemoveObjectsResponse =
VersionId?: string
}
}
+
+export type CopyObjectResultV1 = {
+ etag: string
+ lastModified: string | Date
+}
+export type CopyObjectResultV2 = {
+ Bucket?: string
+ Key?: string
+ LastModified: string | Date
+ MetaData?: ResponseHeader
+ VersionId?: string | null
+ SourceVersionId?: string | null
+ Etag?: string
+ Size?: number
+}
+
+export type CopyObjectResult = CopyObjectResultV1 | CopyObjectResultV2
+export type CopyObjectParams = [CopySourceOptions, CopyDestinationOptions] | [string, string, string, CopyConditions?]
diff --git a/src/internal/xml-parser.ts b/src/internal/xml-parser.ts
index ed71f5a0..8b2fc107 100644
--- a/src/internal/xml-parser.ts
+++ b/src/internal/xml-parser.ts
@@ -8,7 +8,13 @@ import * as errors from '../errors.ts'
import { SelectResults } from '../helpers.ts'
import { isObject, parseXml, readableStream, sanitizeETag, sanitizeObjectKey, toArray } from './helper.ts'
import { readAsString } from './response.ts'
-import type { BucketItemFromList, BucketItemWithMetadata, ObjectLockInfo, ReplicationConfig } from './type.ts'
+import type {
+ BucketItemFromList,
+ BucketItemWithMetadata,
+ CopyObjectResultV1,
+ ObjectLockInfo,
+ ReplicationConfig,
+} from './type.ts'
import { RETENTION_VALIDITY_UNITS } from './type.ts'
// parse XML response for bucket region
@@ -566,3 +572,30 @@ export function removeObjectsParser(xml: string) {
}
return []
}
+
+// parse XML response for copy object
+export function parseCopyObject(xml: string): CopyObjectResultV1 {
+ const result: CopyObjectResultV1 = {
+ etag: '',
+ lastModified: '',
+ }
+
+ let xmlobj = parseXml(xml)
+ if (!xmlobj.CopyObjectResult) {
+ throw new errors.InvalidXMLError('Missing tag: "CopyObjectResult"')
+ }
+ xmlobj = xmlobj.CopyObjectResult
+ if (xmlobj.ETag) {
+ result.etag = xmlobj.ETag.replace(/^"/g, '')
+ .replace(/"$/g, '')
+ .replace(/^"/g, '')
+ .replace(/"$/g, '')
+ .replace(/^"/g, '')
+ .replace(/"$/g, '')
+ }
+ if (xmlobj.LastModified) {
+ result.lastModified = new Date(xmlobj.LastModified)
+ }
+
+ return result
+}
diff --git a/src/minio.d.ts b/src/minio.d.ts
index 4599a391..9ec36cf3 100644
--- a/src/minio.d.ts
+++ b/src/minio.d.ts
@@ -147,20 +147,6 @@ export class Client extends TypedClient {
listObjectsV2(bucketName: string, prefix?: string, recursive?: boolean, startAfter?: string): BucketStream
- copyObject(
- bucketName: string,
- objectName: string,
- sourceObject: string,
- conditions: CopyConditions,
- callback: ResultCallback,
- ): void
- copyObject(
- bucketName: string,
- objectName: string,
- sourceObject: string,
- conditions: CopyConditions,
- ): 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 53cb2473..f2f0816d 100644
--- a/src/minio.js
+++ b/src/minio.js
@@ -22,16 +22,13 @@ import * as querystring from 'query-string'
import xml2js from 'xml2js'
import * as errors from './errors.ts'
-import { CopyDestinationOptions, CopySourceOptions } from './helpers.ts'
+import { CopyDestinationOptions } from './helpers.ts'
import { callbackify } from './internal/callbackify.js'
import { TypedClient } from './internal/client.ts'
import { CopyConditions } from './internal/copy-conditions.ts'
import {
calculateEvenSplits,
- extractMetadata,
getScope,
- getSourceVersionId,
- getVersionId,
isBoolean,
isFunction,
isNumber,
@@ -47,7 +44,6 @@ import {
pipesetup,
sanitizeETag,
uriEscape,
- uriResourceEscape,
} from './internal/helper.ts'
import { PostPolicy } from './internal/post-policy.ts'
import { NotificationConfig, NotificationPoller } from './notification.ts'
@@ -85,136 +81,6 @@ export class Client extends TypedClient {
}
this.userAgent = `${this.userAgent} ${appName}/${appVersion}`
}
- // Copy the object.
- //
- // __Arguments__
- // * `bucketName` _string_: name of the bucket
- // * `objectName` _string_: name of the object
- // * `srcObject` _string_: path of the source object to be copied
- // * `conditions` _CopyConditions_: copy conditions that needs to be satisfied (optional, default `null`)
- // * `callback(err, {etag, lastModified})` _function_: non null `err` indicates error, `etag` _string_ and `listModifed` _Date_ are respectively the etag and the last modified date of the newly copied object
- copyObjectV1(arg1, arg2, arg3, arg4, arg5) {
- var bucketName = arg1
- var objectName = arg2
- var srcObject = arg3
- var conditions, cb
- if (typeof arg4 == 'function' && arg5 === undefined) {
- conditions = null
- cb = arg4
- } else {
- conditions = arg4
- cb = arg5
- }
- if (!isValidBucketName(bucketName)) {
- throw new errors.InvalidBucketNameError('Invalid bucket name: ' + bucketName)
- }
- if (!isValidObjectName(objectName)) {
- throw new errors.InvalidObjectNameError(`Invalid object name: ${objectName}`)
- }
- if (!isString(srcObject)) {
- throw new TypeError('srcObject should be of type "string"')
- }
- if (srcObject === '') {
- throw new errors.InvalidPrefixError(`Empty source prefix`)
- }
-
- if (conditions !== null && !(conditions instanceof CopyConditions)) {
- throw new TypeError('conditions should be of type "CopyConditions"')
- }
-
- var headers = {}
- headers['x-amz-copy-source'] = uriResourceEscape(srcObject)
-
- if (conditions !== null) {
- if (conditions.modified !== '') {
- headers['x-amz-copy-source-if-modified-since'] = conditions.modified
- }
- if (conditions.unmodified !== '') {
- headers['x-amz-copy-source-if-unmodified-since'] = conditions.unmodified
- }
- if (conditions.matchETag !== '') {
- headers['x-amz-copy-source-if-match'] = conditions.matchETag
- }
- if (conditions.matchEtagExcept !== '') {
- headers['x-amz-copy-source-if-none-match'] = conditions.matchETagExcept
- }
- }
-
- var method = 'PUT'
- this.makeRequest({ method, bucketName, objectName, headers }, '', [200], '', true, (e, response) => {
- if (e) {
- return cb(e)
- }
- var transformer = transformers.getCopyObjectTransformer()
- pipesetup(response, transformer)
- .on('error', (e) => cb(e))
- .on('data', (data) => cb(null, data))
- })
- }
-
- /**
- * Internal Method to perform copy of an object.
- * @param sourceConfig __object__ instance of CopySourceOptions @link ./helpers/CopySourceOptions
- * @param destConfig __object__ instance of CopyDestinationOptions @link ./helpers/CopyDestinationOptions
- * @param cb __function__ called with null if there is an error
- * @returns Promise if no callack is passed.
- */
- copyObjectV2(sourceConfig, destConfig, cb) {
- if (!(sourceConfig instanceof CopySourceOptions)) {
- throw new errors.InvalidArgumentError('sourceConfig should of type CopySourceOptions ')
- }
- if (!(destConfig instanceof CopyDestinationOptions)) {
- throw new errors.InvalidArgumentError('destConfig should of type CopyDestinationOptions ')
- }
- if (!destConfig.validate()) {
- return false
- }
- if (!destConfig.validate()) {
- return false
- }
- if (!isFunction(cb)) {
- throw new TypeError('callback should be of type "function"')
- }
-
- const headers = Object.assign({}, sourceConfig.getHeaders(), destConfig.getHeaders())
-
- const bucketName = destConfig.Bucket
- const objectName = destConfig.Object
-
- const method = 'PUT'
- this.makeRequest({ method, bucketName, objectName, headers }, '', [200], '', true, (e, response) => {
- if (e) {
- return cb(e)
- }
- const transformer = transformers.getCopyObjectTransformer()
- pipesetup(response, transformer)
- .on('error', (e) => cb(e))
- .on('data', (data) => {
- const resHeaders = response.headers
-
- const copyObjResponse = {
- Bucket: destConfig.Bucket,
- Key: destConfig.Object,
- LastModified: data.LastModified,
- MetaData: extractMetadata(resHeaders),
- VersionId: getVersionId(resHeaders),
- SourceVersionId: getSourceVersionId(resHeaders),
- Etag: sanitizeETag(resHeaders.etag),
- Size: +resHeaders['content-length'],
- }
-
- return cb(null, copyObjResponse)
- })
- })
- }
-
- // Backward compatibility for Copy Object API.
- copyObject(...allArgs) {
- if (allArgs[0] instanceof CopySourceOptions && allArgs[1] instanceof CopyDestinationOptions) {
- return this.copyObjectV2(...arguments)
- }
- return this.copyObjectV1(...arguments)
- }
// list a batch of objects
listObjectsQuery(bucketName, prefix, marker, listQueryOpts = {}) {
@@ -978,9 +844,6 @@ export class Client extends TypedClient {
}
}
-// Promisify various public-facing APIs on the Client module.
-Client.prototype.copyObject = promisify(Client.prototype.copyObject)
-
Client.prototype.presignedUrl = promisify(Client.prototype.presignedUrl)
Client.prototype.presignedGetObject = promisify(Client.prototype.presignedGetObject)
Client.prototype.presignedPutObject = promisify(Client.prototype.presignedPutObject)
@@ -1032,3 +895,4 @@ Client.prototype.removeBucketEncryption = callbackify(Client.prototype.removeBuc
Client.prototype.getObjectRetention = callbackify(Client.prototype.getObjectRetention)
Client.prototype.removeObjects = callbackify(Client.prototype.removeObjects)
Client.prototype.removeIncompleteUpload = callbackify(Client.prototype.removeIncompleteUpload)
+Client.prototype.copyObject = callbackify(Client.prototype.copyObject)
diff --git a/src/transformers.js b/src/transformers.js
index 9225650a..aa883dd8 100644
--- a/src/transformers.js
+++ b/src/transformers.js
@@ -95,11 +95,6 @@ export function getHashSummer(enableSHA256) {
// Following functions return a stream object that parses XML
// and emits suitable Javascript objects.
-// Parses CopyObject response.
-export function getCopyObjectTransformer() {
- return getConcater(xmlParsers.parseCopyObject)
-}
-
// Parses listObjects response.
export function getListObjectsTransformer() {
return getConcater(xmlParsers.parseListObjects)
diff --git a/src/xml-parsers.js b/src/xml-parsers.js
index 44c1505d..522edc30 100644
--- a/src/xml-parsers.js
+++ b/src/xml-parsers.js
@@ -25,33 +25,6 @@ const fxpWithoutNumParser = new XMLParser({
},
})
-// parse XML response for copy object
-export function parseCopyObject(xml) {
- var result = {
- etag: '',
- lastModified: '',
- }
-
- var xmlobj = parseXml(xml)
- if (!xmlobj.CopyObjectResult) {
- throw new errors.InvalidXMLError('Missing tag: "CopyObjectResult"')
- }
- xmlobj = xmlobj.CopyObjectResult
- if (xmlobj.ETag) {
- result.etag = xmlobj.ETag.replace(/^"/g, '')
- .replace(/"$/g, '')
- .replace(/^"/g, '')
- .replace(/"$/g, '')
- .replace(/^"/g, '')
- .replace(/"$/g, '')
- }
- if (xmlobj.LastModified) {
- result.lastModified = new Date(xmlobj.LastModified)
- }
-
- return result
-}
-
// parse XML response for bucket notification
export function parseBucketNotification(xml) {
var result = {