diff --git a/packages/storage/__tests__/foundation/factories/serviceClients/s3/data/shared/serde/CreateCopyObjectDeserializer.test.ts b/packages/storage/__tests__/foundation/factories/serviceClients/s3/data/shared/serde/CreateCopyObjectDeserializer.test.ts
new file mode 100644
index 00000000000..bc8e46de3ae
--- /dev/null
+++ b/packages/storage/__tests__/foundation/factories/serviceClients/s3/data/shared/serde/CreateCopyObjectDeserializer.test.ts
@@ -0,0 +1,76 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { HttpResponse } from '@aws-amplify/core/internals/aws-client-utils';
+import * as clientUtils from '@aws-amplify/core/internals/aws-client-utils';
+
+import { createCopyObjectDeserializer } from '../../../../../../../../src/foundation/factories/serviceClients/s3/s3data/shared/serde';
+import { StorageError } from '../../../../../../../../src/errors/StorageError';
+
+describe('createCopyObjectDeserializer', () => {
+ const deserializer = createCopyObjectDeserializer();
+
+ it('returns body for 2xx status code', async () => {
+ const response: HttpResponse = {
+ statusCode: 200,
+ headers: {
+ 'x-amz-id-2': 'requestId2',
+ 'x-amz-request-id': 'requestId',
+ },
+ body: {
+ json: () => Promise.resolve({}),
+ blob: () => Promise.resolve(new Blob()),
+ text: () =>
+ Promise.resolve(
+ '' +
+ 'string' +
+ 'timestamp' +
+ 'string' +
+ 'string' +
+ 'string' +
+ 'string' +
+ '',
+ ),
+ },
+ };
+ const output = await deserializer(response);
+
+ expect(output).toEqual(
+ expect.objectContaining({
+ $metadata: {
+ requestId: response.headers['x-amz-request-id'],
+ extendedRequestId: response.headers['x-amz-id-2'],
+ httpStatusCode: 200,
+ },
+ }),
+ );
+ });
+
+ it('throws StorageError for 4xx status code', async () => {
+ const expectedErrorName = 'TestError';
+ const expectedErrorMessage = '400';
+ const expectedError = new Error(expectedErrorMessage);
+ expectedError.name = expectedErrorName;
+
+ jest
+ .spyOn(clientUtils, 'parseJsonError')
+ .mockReturnValueOnce(expectedError as any);
+
+ const response: HttpResponse = {
+ statusCode: 400,
+ body: {
+ json: () => Promise.resolve({}),
+ blob: () => Promise.resolve(new Blob()),
+ text: () => Promise.resolve(''),
+ },
+ headers: {},
+ };
+
+ expect(deserializer(response as any)).rejects.toThrow(
+ new StorageError({
+ name: expectedErrorName,
+ message: expectedErrorMessage,
+ }),
+ );
+ });
+});
diff --git a/packages/storage/__tests__/foundation/factories/serviceClients/s3/data/shared/serde/CreateCopyObjectSerializer.test.ts b/packages/storage/__tests__/foundation/factories/serviceClients/s3/data/shared/serde/CreateCopyObjectSerializer.test.ts
new file mode 100644
index 00000000000..b768d983cd0
--- /dev/null
+++ b/packages/storage/__tests__/foundation/factories/serviceClients/s3/data/shared/serde/CreateCopyObjectSerializer.test.ts
@@ -0,0 +1,37 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { AmplifyUrl } from '@aws-amplify/core/internals/utils';
+
+import { createCopyObjectSerializer } from '../../../../../../../../src/foundation/factories/serviceClients/s3/s3data/shared/serde';
+
+describe('createCopyObjectSerializer', () => {
+ it('should serialize copyObject request', async () => {
+ const input = {
+ Bucket: 'bucket',
+ CopySource: 'sourceBucket/sourceKey',
+ Key: 'mykey',
+ CacheControl: 'cacheControl',
+ ContentType: 'contentType',
+ ACL: 'acl',
+ };
+ const endPointUrl = 'http://test.com';
+ const endpoint = { url: new AmplifyUrl(endPointUrl) };
+
+ const serializer = createCopyObjectSerializer();
+ const result = await serializer(input, endpoint);
+
+ expect(result).toEqual({
+ url: expect.objectContaining({
+ href: `${endPointUrl}/${input.Key}`,
+ }),
+ method: 'PUT',
+ headers: expect.objectContaining({
+ 'x-amz-copy-source': 'sourceBucket/sourceKey',
+ 'cache-control': 'cacheControl',
+ 'content-type': 'contentType',
+ 'x-amz-acl': 'acl',
+ }),
+ });
+ });
+});
diff --git a/packages/storage/__tests__/providers/s3/apis/copy.test.ts b/packages/storage/__tests__/providers/s3/apis/copy.test.ts
index 56104e84d17..faaba0b87c2 100644
--- a/packages/storage/__tests__/providers/s3/apis/copy.test.ts
+++ b/packages/storage/__tests__/providers/s3/apis/copy.test.ts
@@ -6,7 +6,7 @@ import { Amplify, StorageAccessLevel } from '@aws-amplify/core';
import { StorageError } from '../../../../src/errors/StorageError';
import { StorageValidationErrorCode } from '../../../../src/errors/types/validation';
-import { copyObject } from '../../../../src/providers/s3/utils/client/s3data';
+import { createCopyObjectClient } from '../../../../src/foundation/factories/serviceClients/s3';
import { copy } from '../../../../src/providers/s3/apis';
import {
CopyInput,
@@ -17,7 +17,7 @@ import {
import './testUtils';
import { BucketInfo } from '../../../../src/providers/s3/types/options';
-jest.mock('../../../../src/providers/s3/utils/client/s3data');
+jest.mock('../../../../src/foundation/factories/serviceClients/s3');
jest.mock('@aws-amplify/core', () => ({
ConsoleLogger: jest.fn().mockImplementation(function ConsoleLogger() {
return { debug: jest.fn() };
@@ -29,7 +29,10 @@ jest.mock('@aws-amplify/core', () => ({
},
},
}));
-const mockCopyObject = copyObject as jest.Mock;
+
+const mockCopyObject = jest.fn();
+const mockCreateCopyObjectClient = jest.mocked(createCopyObjectClient);
+
const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock;
const mockGetConfig = Amplify.getConfig as jest.Mock;
@@ -81,6 +84,7 @@ describe('copy API', () => {
Metadata: { key: 'value' },
};
});
+ mockCreateCopyObjectClient.mockReturnValueOnce(mockCopyObject);
});
afterEach(() => {
jest.clearAllMocks();
@@ -188,8 +192,8 @@ describe('copy API', () => {
},
});
expect(key).toEqual(destinationKey);
- expect(copyObject).toHaveBeenCalledTimes(1);
- await expect(copyObject).toBeLastCalledWithConfigAndInput(
+ expect(mockCopyObject).toHaveBeenCalledTimes(1);
+ await expect(mockCopyObject).toBeLastCalledWithConfigAndInput(
copyObjectClientConfig,
{
...copyObjectClientBaseParams,
@@ -213,8 +217,8 @@ describe('copy API', () => {
bucket: bucketInfo,
},
});
- expect(copyObject).toHaveBeenCalledTimes(1);
- await expect(copyObject).toBeLastCalledWithConfigAndInput(
+ expect(mockCopyObject).toHaveBeenCalledTimes(1);
+ await expect(mockCopyObject).toBeLastCalledWithConfigAndInput(
{
credentials,
region: bucketInfo.region,
@@ -241,6 +245,7 @@ describe('copy API', () => {
Metadata: { key: 'value' },
};
});
+ mockCreateCopyObjectClient.mockReturnValueOnce(mockCopyObject);
});
afterEach(() => {
jest.clearAllMocks();
@@ -272,8 +277,8 @@ describe('copy API', () => {
destination: { path: destinationPath },
});
expect(path).toEqual(expectedDestinationPath);
- expect(copyObject).toHaveBeenCalledTimes(1);
- await expect(copyObject).toBeLastCalledWithConfigAndInput(
+ expect(mockCopyObject).toHaveBeenCalledTimes(1);
+ await expect(mockCopyObject).toBeLastCalledWithConfigAndInput(
copyObjectClientConfig,
{
...copyObjectClientBaseParams,
@@ -295,8 +300,8 @@ describe('copy API', () => {
bucket: bucketInfo,
},
});
- expect(copyObject).toHaveBeenCalledTimes(1);
- await expect(copyObject).toBeLastCalledWithConfigAndInput(
+ expect(mockCopyObject).toHaveBeenCalledTimes(1);
+ await expect(mockCopyObject).toBeLastCalledWithConfigAndInput(
{
credentials,
region: bucketInfo.region,
@@ -324,6 +329,7 @@ describe('copy API', () => {
name: 'NotFound',
}),
);
+ mockCreateCopyObjectClient.mockReturnValueOnce(mockCopyObject);
expect.assertions(3);
const missingSourceKey = 'SourceKeyNotFound';
try {
@@ -332,8 +338,8 @@ describe('copy API', () => {
destination: { key: destinationKey },
});
} catch (error: any) {
- expect(copyObject).toHaveBeenCalledTimes(1);
- await expect(copyObject).toBeLastCalledWithConfigAndInput(
+ expect(mockCopyObject).toHaveBeenCalledTimes(1);
+ await expect(mockCopyObject).toBeLastCalledWithConfigAndInput(
copyObjectClientConfig,
{
...copyObjectClientBaseParams,
diff --git a/packages/storage/src/foundation/factories/serviceClients/s3/index.ts b/packages/storage/src/foundation/factories/serviceClients/s3/index.ts
index 050bfa63351..db25f3a2e3b 100644
--- a/packages/storage/src/foundation/factories/serviceClients/s3/index.ts
+++ b/packages/storage/src/foundation/factories/serviceClients/s3/index.ts
@@ -1,4 +1,4 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-export { createDeleteObjectClient } from './s3data/createDeleteObjectClient';
+export { createDeleteObjectClient, createCopyObjectClient } from './s3data';
diff --git a/packages/storage/src/foundation/factories/serviceClients/s3/s3data/createCopyObjectClient.ts b/packages/storage/src/foundation/factories/serviceClients/s3/s3data/createCopyObjectClient.ts
new file mode 100644
index 00000000000..330cda31a42
--- /dev/null
+++ b/packages/storage/src/foundation/factories/serviceClients/s3/s3data/createCopyObjectClient.ts
@@ -0,0 +1,21 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { composeServiceApi } from '@aws-amplify/core/internals/aws-client-utils/composers';
+
+import { s3TransferHandler } from '../../../../dI';
+
+import {
+ createCopyObjectDeserializer,
+ createCopyObjectSerializer,
+} from './shared/serde';
+import { DEFAULT_SERVICE_CLIENT_API_CONFIG } from './constants';
+
+export const createCopyObjectClient = () => {
+ return composeServiceApi(
+ s3TransferHandler,
+ createCopyObjectSerializer(),
+ createCopyObjectDeserializer(),
+ { ...DEFAULT_SERVICE_CLIENT_API_CONFIG, responseType: 'text' },
+ );
+};
diff --git a/packages/storage/src/foundation/factories/serviceClients/s3/s3data/index.ts b/packages/storage/src/foundation/factories/serviceClients/s3/s3data/index.ts
index fd2f388c326..3c184be5b4a 100644
--- a/packages/storage/src/foundation/factories/serviceClients/s3/s3data/index.ts
+++ b/packages/storage/src/foundation/factories/serviceClients/s3/s3data/index.ts
@@ -2,3 +2,4 @@
// SPDX-License-Identifier: Apache-2.0
export { createDeleteObjectClient } from './createDeleteObjectClient';
+export { createCopyObjectClient } from './createCopyObjectClient';
diff --git a/packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/CreateCopyObjectDeserializer.ts b/packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/CreateCopyObjectDeserializer.ts
new file mode 100644
index 00000000000..bbbdfc6c03a
--- /dev/null
+++ b/packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/CreateCopyObjectDeserializer.ts
@@ -0,0 +1,28 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import {
+ HttpResponse,
+ parseMetadata,
+} from '@aws-amplify/core/internals/aws-client-utils';
+
+import { buildStorageServiceError } from '../../../deserializeHelpers';
+import { parseXmlBody, parseXmlError } from '../../../parsePayload';
+import type { CopyObjectCommandOutput } from '../../types';
+
+type CopyObjectOutput = CopyObjectCommandOutput;
+
+export const createCopyObjectDeserializer =
+ (): ((response: HttpResponse) => Promise) =>
+ async (response: HttpResponse): Promise => {
+ if (response.statusCode >= 300) {
+ const error = (await parseXmlError(response)) as Error;
+ throw buildStorageServiceError(error, response.statusCode);
+ } else {
+ await parseXmlBody(response);
+
+ return {
+ $metadata: parseMetadata(response),
+ };
+ }
+ };
diff --git a/packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/CreateCopyObjectSerializer.ts b/packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/CreateCopyObjectSerializer.ts
new file mode 100644
index 00000000000..1147695c07e
--- /dev/null
+++ b/packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/CreateCopyObjectSerializer.ts
@@ -0,0 +1,53 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import {
+ Endpoint,
+ HttpRequest,
+} from '@aws-amplify/core/internals/aws-client-utils';
+import { AmplifyUrl } from '@aws-amplify/core/internals/utils';
+
+import {
+ assignStringVariables,
+ serializeObjectConfigsToHeaders,
+ serializePathnameObjectKey,
+ validateS3RequiredParameter,
+} from '../../../serializeHelpers';
+import type { CopyObjectCommandInput } from '../../types';
+
+type CopyObjectInput = Pick<
+ CopyObjectCommandInput,
+ | 'Bucket'
+ | 'CopySource'
+ | 'Key'
+ | 'MetadataDirective'
+ | 'CacheControl'
+ | 'ContentType'
+ | 'ContentDisposition'
+ | 'ContentLanguage'
+ | 'Expires'
+ | 'ACL'
+ | 'Tagging'
+ | 'Metadata'
+>;
+
+export const createCopyObjectSerializer =
+ (): ((input: CopyObjectInput, endpoint: Endpoint) => Promise) =>
+ async (input: CopyObjectInput, endpoint: Endpoint): Promise => {
+ const headers = {
+ ...(await serializeObjectConfigsToHeaders(input)),
+ ...assignStringVariables({
+ 'x-amz-copy-source': input.CopySource,
+ 'x-amz-metadata-directive': input.MetadataDirective,
+ }),
+ };
+ const url = new AmplifyUrl(endpoint.url.toString());
+ validateS3RequiredParameter(!!input.Key, 'Key');
+ url.pathname = serializePathnameObjectKey(url, input.Key);
+
+ return {
+ method: 'PUT',
+ headers,
+ url,
+ };
+ };
diff --git a/packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/index.ts b/packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/index.ts
index eda9a6571ec..ff83f8334cd 100644
--- a/packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/index.ts
+++ b/packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/index.ts
@@ -3,3 +3,5 @@
export { createDeleteObjectSerializer } from './CreateDeleteObjectSerializer';
export { createDeleteObjectDeserializer } from './CreateDeleteObjectDeserializer';
+export { createCopyObjectDeserializer } from './CreateCopyObjectDeserializer';
+export { createCopyObjectSerializer } from './CreateCopyObjectSerializer';
diff --git a/packages/storage/src/providers/s3/apis/internal/copy.ts b/packages/storage/src/providers/s3/apis/internal/copy.ts
index 9119917efa1..7cf32c94ed1 100644
--- a/packages/storage/src/providers/s3/apis/internal/copy.ts
+++ b/packages/storage/src/providers/s3/apis/internal/copy.ts
@@ -18,7 +18,7 @@ import {
} from '../../utils';
import { StorageValidationErrorCode } from '../../../../errors/types/validation';
import { assertValidationError } from '../../../../errors/utils/assertValidationError';
-import { copyObject } from '../../utils/client/s3data';
+import { createCopyObjectClient } from '../../../../foundation/factories/serviceClients/s3';
import { getStorageUserAgentValue } from '../../utils/userAgent';
import { logger } from '../../../../utils';
@@ -180,6 +180,7 @@ const serviceCopy = async ({
bucket: string;
s3Config: ResolvedS3Config;
}) => {
+ const copyObject = createCopyObjectClient();
await copyObject(
{
...s3Config,