Skip to content

Commit 11d4190

Browse files
authored
feat(storage): support tagging directive in copy (#14527)
1 parent f8f2904 commit 11d4190

File tree

6 files changed

+80
-0
lines changed

6 files changed

+80
-0
lines changed

packages/storage/__tests__/providers/s3/apis/internal/copy.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ describe('copy API', () => {
196196
...copyObjectClientBaseParams,
197197
CopySource: expectedSourceKey,
198198
Key: expectedDestinationKey,
199+
TaggingDirective: 'COPY',
199200
},
200201
);
201202
});
@@ -226,6 +227,7 @@ describe('copy API', () => {
226227
MetadataDirective: 'COPY',
227228
CopySource: `${bucket}/public/sourceKey`,
228229
Key: 'public/destinationKey',
230+
TaggingDirective: 'COPY',
229231
},
230232
);
231233
});
@@ -340,6 +342,7 @@ describe('copy API', () => {
340342
...copyObjectClientBaseParams,
341343
CopySource: `${bucket}/${expectedSourcePath}`,
342344
Key: expectedDestinationPath,
345+
TaggingDirective: 'COPY',
343346
},
344347
);
345348
},
@@ -368,6 +371,7 @@ describe('copy API', () => {
368371
MetadataDirective: 'COPY',
369372
CopySource: `${bucket}/sourcePath`,
370373
Key: 'destinationPath',
374+
TaggingDirective: 'COPY',
371375
},
372376
);
373377
});
@@ -431,6 +435,49 @@ describe('copy API', () => {
431435
);
432436
});
433437
});
438+
439+
describe('TagConfig passed in input', () => {
440+
it('should use COPY TaggingDirective when tagConfig is not provided (default)', async () => {
441+
await copyWrapper({
442+
source: { path: 'sourcePath' },
443+
destination: { path: 'destinationPath' },
444+
});
445+
446+
expect(copyObject).toHaveBeenCalledWith(
447+
expect.any(Object),
448+
expect.objectContaining({
449+
TaggingDirective: 'COPY',
450+
}),
451+
);
452+
});
453+
454+
it('should use COPY TaggingDirective when tagConfig mode is copy', async () => {
455+
await copyWrapper({
456+
source: { path: 'sourcePath' },
457+
destination: { path: 'destinationPath' },
458+
tagConfig: { mode: 'copy' },
459+
});
460+
461+
expect(copyObject).toHaveBeenCalledWith(
462+
expect.any(Object),
463+
expect.objectContaining({
464+
TaggingDirective: 'COPY',
465+
}),
466+
);
467+
});
468+
469+
it('should use REPLACE TaggingDirective with empty tagging when tagConfig mode is remove', async () => {
470+
await copyWrapper({
471+
source: { path: 'sourcePath' },
472+
destination: { path: 'destinationPath' },
473+
tagConfig: { mode: 'remove' },
474+
});
475+
476+
const callArgs = mockCopyObject.mock.calls[0][1];
477+
expect(callArgs.TaggingDirective).toBe('REPLACE');
478+
expect(callArgs.Tagging).toBeUndefined();
479+
});
480+
});
434481
});
435482
});
436483

@@ -460,6 +507,7 @@ describe('copy API', () => {
460507
...copyObjectClientBaseParams,
461508
CopySource: `${bucket}/public/${missingSourceKey}`,
462509
Key: `public/${destinationKey}`,
510+
TaggingDirective: 'COPY',
463511
},
464512
);
465513
expect(error.$metadata.httpStatusCode).toBe(404);

packages/storage/__tests__/providers/s3/utils/client/s3Data/copyObject.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,14 @@ describe('validateCopyObjectHeaders', () => {
107107
request: {
108108
...baseRequest,
109109
MetadataDirective: 'mock-metadata',
110+
TaggingDirective: 'COPY',
110111
CopySourceIfMatch: 'mock-etag',
111112
CopySourceIfUnmodifiedSince: new Date(0),
112113
},
113114
headers: {
114115
...baseHeaders,
115116
'x-amz-metadata-directive': 'mock-metadata',
117+
'x-amz-tagging-directive': 'COPY',
116118
'x-amz-copy-source-if-match': 'mock-etag',
117119
'x-amz-copy-source-if-unmodified-since':
118120
'Thu, 01 Jan 1970 00:00:00 GMT',

packages/storage/src/internals/types/inputs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ type ExtendCopyInputWithAdvancedOptions<InputType, ExtendedOptionsType> =
137137
? {
138138
source: InputType['source'];
139139
destination: InputType['destination'];
140+
tagConfig?: InputType['tagConfig'];
140141
options?: ExtendedOptionsType;
141142
}
142143
: never;

packages/storage/src/providers/s3/apis/internal/copy.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
CopyWithPathInput,
1111
CopyWithPathOutput,
1212
} from '../../types';
13+
import { TagConfig } from '../../../../types/inputs';
1314
import { ResolvedS3Config, StorageBucket } from '../../types/options';
1415
import {
1516
isInputWithPath,
@@ -114,6 +115,7 @@ const copyWithPath = async (
114115
eTag: input.source.eTag,
115116
expectedSourceBucketOwner: input.source?.expectedBucketOwner,
116117
expectedBucketOwner: input.destination?.expectedBucketOwner,
118+
tagConfig: input.tagConfig,
117119
});
118120

119121
return { path: finalCopyDestination };
@@ -193,6 +195,7 @@ const serviceCopy = async ({
193195
eTag,
194196
expectedSourceBucketOwner,
195197
expectedBucketOwner,
198+
tagConfig,
196199
}: {
197200
source: string;
198201
destination: string;
@@ -202,6 +205,7 @@ const serviceCopy = async ({
202205
eTag?: string;
203206
expectedSourceBucketOwner?: string;
204207
expectedBucketOwner?: string;
208+
tagConfig?: TagConfig;
205209
}) => {
206210
await copyObject(
207211
{
@@ -213,6 +217,7 @@ const serviceCopy = async ({
213217
CopySource: source,
214218
Key: destination,
215219
MetadataDirective: 'COPY', // Copies over metadata like contentType as well
220+
TaggingDirective: tagConfig?.mode === 'remove' ? 'REPLACE' : 'COPY',
216221
CopySourceIfMatch: eTag,
217222
CopySourceIfUnmodifiedSince: notModifiedSince,
218223
ExpectedSourceBucketOwner: expectedSourceBucketOwner,

packages/storage/src/providers/s3/utils/client/s3data/copyObject.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export type CopyObjectInput = Pick<
3535
| 'CopySource'
3636
| 'Key'
3737
| 'MetadataDirective'
38+
| 'TaggingDirective'
3839
| 'CacheControl'
3940
| 'ContentType'
4041
| 'ContentDisposition'
@@ -60,6 +61,7 @@ const copyObjectSerializer = async (
6061
...assignStringVariables({
6162
'x-amz-copy-source': input.CopySource,
6263
'x-amz-metadata-directive': input.MetadataDirective,
64+
'x-amz-tagging-directive': input.TaggingDirective,
6365
'x-amz-copy-source-if-match': input.CopySourceIfMatch,
6466
'x-amz-copy-source-if-unmodified-since':
6567
input.CopySourceIfUnmodifiedSince?.toUTCString(),
@@ -97,6 +99,7 @@ export const validateCopyObjectHeaders = (
9799
input.MetadataDirective,
98100
headers['x-amz-metadata-directive'],
99101
),
102+
bothNilOrEqual(input.TaggingDirective, headers['x-amz-tagging-directive']),
100103
bothNilOrEqual(
101104
input.CopySourceIfMatch,
102105
headers['x-amz-copy-source-if-match'],

packages/storage/src/types/inputs.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,30 @@ export interface StorageCopyInputWithKey<
9595
destination: DestinationOptions;
9696
}
9797

98+
export interface CopyTagConfig {
99+
mode: 'copy';
100+
}
101+
102+
export interface RemoveTagConfig {
103+
mode: 'remove';
104+
}
105+
106+
export interface ReplaceTagConfig {
107+
mode: 'replace';
108+
tags: Record<string, string>;
109+
}
110+
111+
export type TagConfigInternal =
112+
| CopyTagConfig
113+
| RemoveTagConfig
114+
| ReplaceTagConfig;
115+
116+
export type TagConfig = Exclude<TagConfigInternal, ReplaceTagConfig>;
117+
98118
export interface StorageCopyInputWithPath {
99119
source: StorageOperationInputWithPath & CopyWithPathSourceOptions;
100120
destination: StorageOperationInputWithPath & CopyWithPathDestinationOptions;
121+
tagConfig?: TagConfig;
101122
}
102123

103124
/**

0 commit comments

Comments
 (0)