Skip to content

Commit

Permalink
migrate copyObject api to ts (minio#1289)
Browse files Browse the repository at this point in the history
  • Loading branch information
prakashsvmx authored May 20, 2024
1 parent da24d11 commit 8633968
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 215 deletions.
23 changes: 8 additions & 15 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -1089,32 +1089,25 @@ minioClient.fPutObject('mybucket', '40mbfile', file, metaData, function (err, ob

<a name="copyObject"></a>

### 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)
```

<a name="statObject"></a>
Expand Down
16 changes: 2 additions & 14 deletions examples/copy-object.js → examples/copy-object.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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)
137 changes: 136 additions & 1 deletion src/internal/client.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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,
Expand Down Expand Up @@ -59,6 +69,9 @@ import type {
BucketItemStat,
BucketStream,
BucketVersioningConfiguration,
CopyObjectParams,
CopyObjectResult,
CopyObjectResultV2,
EncryptionConfig,
GetObjectLegalHoldOptions,
GetObjectRetentionOpts,
Expand Down Expand Up @@ -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<CopyObjectResultV2> {
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<CopyObjectResult>
async copyObject(
targetBucketName: string,
targetObjectName: string,
sourceBucketNameAndObjectName: string,
conditions?: CopyConditions,
): Promise<CopyObjectResult>
async copyObject(...allArgs: CopyObjectParams): Promise<CopyObjectResult> {
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)
}
}
21 changes: 21 additions & 0 deletions src/internal/type.ts
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down Expand Up @@ -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?]
35 changes: 34 additions & 1 deletion src/internal/xml-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(/^&quot;/g, '')
.replace(/&quot;$/g, '')
.replace(/^&#34;/g, '')
.replace(/&#34;$/g, '')
}
if (xmlobj.LastModified) {
result.lastModified = new Date(xmlobj.LastModified)
}

return result
}
14 changes: 0 additions & 14 deletions src/minio.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,20 +147,6 @@ export class Client extends TypedClient {

listObjectsV2(bucketName: string, prefix?: string, recursive?: boolean, startAfter?: string): BucketStream<BucketItem>

copyObject(
bucketName: string,
objectName: string,
sourceObject: string,
conditions: CopyConditions,
callback: ResultCallback<BucketItemCopy>,
): void
copyObject(
bucketName: string,
objectName: string,
sourceObject: string,
conditions: CopyConditions,
): Promise<BucketItemCopy>

removeIncompleteUpload(bucketName: string, objectName: string, callback: NoResultCallback): void
removeIncompleteUpload(bucketName: string, objectName: string): Promise<void>
composeObject(
Expand Down
Loading

0 comments on commit 8633968

Please sign in to comment.