Skip to content

Commit 4189c66

Browse files
aadimchsarayev
andauthored
feat(storage): add cacheControl header to uploadData, downloadData and getUrl (aws-amplify#14410)
feat: add cacheControl header to uploadData, downloadData and getUrl Co-authored-by: sarayev <126112721+sarayev@users.noreply.github.com>
1 parent 0035497 commit 4189c66

File tree

10 files changed

+276
-4
lines changed

10 files changed

+276
-4
lines changed

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,48 @@ describe('downloadData with key', () => {
310310
);
311311
});
312312
});
313+
314+
describe('ResponseCacheControl passed in options', () => {
315+
it('should include cacheControl in headers when provided', async () => {
316+
(getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' });
317+
downloadData({
318+
path: inputKey,
319+
options: {
320+
cacheControl: 'no-store',
321+
},
322+
});
323+
324+
const { job } = mockCreateDownloadTask.mock.calls[0][0];
325+
await job();
326+
327+
expect(getObject).toHaveBeenCalledTimes(1);
328+
await expect(getObject).toBeLastCalledWithConfigAndInput(
329+
expect.any(Object),
330+
expect.objectContaining({
331+
ResponseCacheControl: 'no-store',
332+
}),
333+
);
334+
});
335+
336+
it('should NOT include cacheControl in headers when not provided', async () => {
337+
(getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' });
338+
downloadData({
339+
path: inputKey,
340+
});
341+
342+
const { job } = mockCreateDownloadTask.mock.calls[0][0];
343+
await job();
344+
345+
expect(getObject).toHaveBeenCalledTimes(1);
346+
await expect(getObject).toBeLastCalledWithConfigAndInput(
347+
expect.any(Object),
348+
{
349+
Bucket: bucket,
350+
Key: 'public/key',
351+
},
352+
);
353+
});
354+
});
313355
});
314356

315357
describe('downloadData with path', () => {
@@ -544,4 +586,46 @@ describe('downloadData with path', () => {
544586
);
545587
});
546588
});
589+
590+
describe('ResponseCacheControl passed in options', () => {
591+
it('should include cacheControl in headers when provided', async () => {
592+
(getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' });
593+
downloadData({
594+
path: inputKey,
595+
options: {
596+
cacheControl: 'no-store',
597+
},
598+
});
599+
600+
const { job } = mockCreateDownloadTask.mock.calls[0][0];
601+
await job();
602+
603+
expect(getObject).toHaveBeenCalledTimes(1);
604+
await expect(getObject).toBeLastCalledWithConfigAndInput(
605+
expect.any(Object),
606+
expect.objectContaining({
607+
ResponseCacheControl: 'no-store',
608+
}),
609+
);
610+
});
611+
612+
it('should NOT include cacheControl in headers when not provided', async () => {
613+
(getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' });
614+
downloadData({
615+
path: inputKey,
616+
});
617+
618+
const { job } = mockCreateDownloadTask.mock.calls[0][0];
619+
await job();
620+
621+
expect(getObject).toHaveBeenCalledTimes(1);
622+
await expect(getObject).toBeLastCalledWithConfigAndInput(
623+
expect.any(Object),
624+
{
625+
Bucket: bucket,
626+
Key: inputPath,
627+
},
628+
);
629+
});
630+
});
547631
});

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

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,49 @@ describe('getUrl test with key', () => {
182182
);
183183
});
184184
});
185+
186+
describe('cacheControl passed in options', () => {
187+
it('should include ResponseCacheControl header', async () => {
188+
const cacheControl = 'no-store';
189+
await getUrlWrapper({
190+
key: 'key',
191+
options: {
192+
cacheControl,
193+
},
194+
});
195+
expect(getPresignedGetObjectUrl).toHaveBeenCalledTimes(1);
196+
await expect(getPresignedGetObjectUrl).toBeLastCalledWithConfigAndInput(
197+
{
198+
credentials,
199+
region,
200+
expiration: expect.any(Number),
201+
},
202+
{
203+
Bucket: bucket,
204+
Key: 'public/key',
205+
ResponseCacheControl: cacheControl,
206+
},
207+
);
208+
});
209+
210+
it('should NOT include ResponseCacheControl header', async () => {
211+
await getUrlWrapper({
212+
key: 'key',
213+
});
214+
expect(getPresignedGetObjectUrl).toHaveBeenCalledTimes(1);
215+
await expect(getPresignedGetObjectUrl).toBeLastCalledWithConfigAndInput(
216+
{
217+
credentials,
218+
region,
219+
expiration: expect.any(Number),
220+
},
221+
{
222+
Bucket: bucket,
223+
Key: 'public/key',
224+
},
225+
);
226+
});
227+
});
185228
});
186229
describe('Error cases : With key', () => {
187230
afterAll(() => {
@@ -330,6 +373,51 @@ describe('getUrl test with path', () => {
330373
);
331374
});
332375
});
376+
377+
describe('cacheControl passed in options', () => {
378+
it('should include ResponseCacheControl header', async () => {
379+
const inputPath = 'path/';
380+
const cacheControl = 'no-store';
381+
await getUrlWrapper({
382+
path: inputPath,
383+
options: {
384+
cacheControl,
385+
},
386+
});
387+
expect(getPresignedGetObjectUrl).toHaveBeenCalledTimes(1);
388+
await expect(getPresignedGetObjectUrl).toBeLastCalledWithConfigAndInput(
389+
{
390+
credentials,
391+
region,
392+
expiration: expect.any(Number),
393+
},
394+
{
395+
Bucket: bucket,
396+
Key: inputPath,
397+
ResponseCacheControl: cacheControl,
398+
},
399+
);
400+
});
401+
402+
it('should not include ResponseCacheControl header', async () => {
403+
const inputPath = 'path/';
404+
await getUrlWrapper({
405+
path: inputPath,
406+
});
407+
expect(getPresignedGetObjectUrl).toHaveBeenCalledTimes(1);
408+
await expect(getPresignedGetObjectUrl).toBeLastCalledWithConfigAndInput(
409+
{
410+
credentials,
411+
region,
412+
expiration: expect.any(Number),
413+
},
414+
{
415+
Bucket: bucket,
416+
Key: inputPath,
417+
},
418+
);
419+
});
420+
});
333421
});
334422
describe('Happy cases: With path and Content Disposition, Content Type', () => {
335423
const config = {

packages/storage/__tests__/providers/s3/apis/internal/uploadData/putObjectJob.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,30 @@ describe('putObjectJob with key', () => {
235235
);
236236
});
237237
});
238+
239+
describe('cacheControl passed in option', () => {
240+
it('should include CacheControl header', async () => {
241+
const job = putObjectJob(
242+
{
243+
path: testPath,
244+
data,
245+
options: {
246+
cacheControl: 'no-store',
247+
},
248+
},
249+
new AbortController().signal,
250+
dataLength,
251+
);
252+
await job();
253+
254+
await expect(mockPutObject).toBeLastCalledWithConfigAndInput(
255+
expect.objectContaining({ credentials, region }),
256+
expect.objectContaining({
257+
CacheControl: 'no-store',
258+
}),
259+
);
260+
});
261+
});
238262
});
239263

240264
describe('putObjectJob with path', () => {
@@ -520,4 +544,50 @@ describe('putObjectJob with path', () => {
520544
);
521545
});
522546
});
547+
548+
describe('cacheControl passed in option', () => {
549+
it('should include CacheControl header', async () => {
550+
const job = putObjectJob(
551+
{
552+
path: testPath,
553+
data,
554+
options: {
555+
cacheControl: 'no-store',
556+
},
557+
},
558+
new AbortController().signal,
559+
dataLength,
560+
);
561+
await job();
562+
563+
await expect(mockPutObject).toBeLastCalledWithConfigAndInput(
564+
expect.objectContaining({ credentials, region }),
565+
expect.objectContaining({
566+
CacheControl: 'no-store',
567+
}),
568+
);
569+
});
570+
571+
it('should NOT include CacheControl header', async () => {
572+
const job = putObjectJob(
573+
{
574+
path: testPath,
575+
data,
576+
},
577+
new AbortController().signal,
578+
dataLength,
579+
);
580+
await job();
581+
582+
await expect(mockPutObject).toBeLastCalledWithConfigAndInput(
583+
expect.objectContaining({ credentials, region }),
584+
{
585+
Bucket: bucket,
586+
Key: testPath,
587+
Body: data,
588+
ContentType: 'application/octet-stream',
589+
},
590+
);
591+
});
592+
});
523593
});

packages/storage/__tests__/providers/s3/utils/client/S3/cases/headObject.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const headObjectHappyCase: ApiFunctionalTestCase<typeof headObject> = [
2222
},
2323
expect.objectContaining({
2424
url: expect.objectContaining({
25-
href: 'https://bucket.s3.us-east-1.amazonaws.com/key',
25+
href: 'https://bucket.s3.us-east-1.amazonaws.com/key?response-cache-control=no-store',
2626
}),
2727
method: 'HEAD',
2828
}),
@@ -65,7 +65,7 @@ const headObjectHappyCaseCustomEndpoint: ApiFunctionalTestCase<
6565
},
6666
expect.objectContaining({
6767
url: expect.objectContaining({
68-
href: 'https://custom.endpoint.com/bucket/key',
68+
href: 'https://custom.endpoint.com/bucket/key?response-cache-control=no-store',
6969
}),
7070
}),
7171
{

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ const downloadDataJob =
7777
...(downloadDataOptions?.bytesRange && {
7878
Range: `bytes=${downloadDataOptions.bytesRange.start}-${downloadDataOptions.bytesRange.end}`,
7979
}),
80+
ResponseCacheControl: downloadDataOptions?.cacheControl,
8081
ExpectedBucketOwner: downloadDataOptions?.expectedBucketOwner,
8182
},
8283
);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export const getUrl = async (
8282
...(getUrlOptions?.contentType && {
8383
ResponseContentType: getUrlOptions.contentType,
8484
}),
85+
ResponseCacheControl: getUrlOptions?.cacheControl,
8586
ExpectedBucketOwner: getUrlOptions?.expectedBucketOwner,
8687
},
8788
),

packages/storage/src/providers/s3/apis/internal/uploadData/putObjectJob.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export const putObjectJob =
6969
checksumAlgorithm,
7070
onProgress,
7171
expectedBucketOwner,
72+
cacheControl,
7273
} = uploadDataOptions ?? {};
7374

7475
const checksumCRC32 =
@@ -96,6 +97,7 @@ export const putObjectJob =
9697
ContentType: contentType,
9798
ContentDisposition: constructContentDisposition(contentDisposition),
9899
ContentEncoding: contentEncoding,
100+
CacheControl: cacheControl,
99101
Metadata: metadata,
100102
ContentMD5: contentMD5,
101103
ChecksumCRC32: checksumCRC32,

packages/storage/src/providers/s3/types/options.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,11 @@ export type GetUrlOptions = CommonOptions & {
181181
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
182182
*/
183183
contentType?: string;
184+
/**
185+
* The cache-control header value of the file when downloading it.
186+
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
187+
*/
188+
cacheControl?: string;
184189
};
185190

186191
/** @deprecated Use {@link GetUrlWithPathOptions} instead. */
@@ -192,7 +197,13 @@ export type GetUrlWithPathOptions = GetUrlOptions;
192197
*/
193198
export type DownloadDataOptions = CommonOptions &
194199
TransferOptions &
195-
BytesRangeOptions;
200+
BytesRangeOptions & {
201+
/**
202+
* The cache-control header value of the file when downloading it.
203+
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
204+
*/
205+
cacheControl?: string;
206+
};
196207

197208
/** @deprecated Use {@link DownloadDataWithPathOptions} instead. */
198209
export type DownloadDataWithKeyOptions = ReadOptions & DownloadDataOptions;
@@ -236,6 +247,11 @@ export type UploadDataOptions = CommonOptions &
236247
* @default undefined
237248
*/
238249
checksumAlgorithm?: UploadDataChecksumAlgorithm;
250+
/**
251+
* The cache-control header value of the file when downloading it.
252+
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
253+
*/
254+
cacheControl?: string;
239255
};
240256

241257
/** @deprecated Use {@link UploadDataWithPathOptions} instead. */

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export type GetObjectInput = Pick<
5252
| 'ResponseContentDisposition'
5353
| 'ResponseContentType'
5454
| 'ExpectedBucketOwner'
55+
| 'ResponseCacheControl'
5556
>;
5657

5758
export type GetObjectOutput = GetObjectCommandOutput;
@@ -65,6 +66,9 @@ const getObjectSerializer = async (
6566
url.pathname = serializePathnameObjectKey(url, input.Key);
6667
url.search = new AmplifyUrlSearchParams({
6768
'x-id': 'GetObject',
69+
...(input.ResponseCacheControl && {
70+
'response-cache-control': input.ResponseCacheControl,
71+
}),
6872
}).toString();
6973
validateObjectUrl({
7074
bucketName: input.Bucket,

0 commit comments

Comments
 (0)