Skip to content

Commit 865a9cd

Browse files
authored
feat(storage-s3): dynamic presigned URL downloads (#12706)
Previously, if you enabled presigned URL downloads for a collection, all the files would use them. However, it might be possible that you want to use presigned URLs only for specific files (like videos), this PR allows you to pass `shouldUseSignedURL` to control that behavior dynamically. ```ts s3Storage({ collections: { media: { signedDownloads: { shouldUseSignedURL: ({ collection, filename, req }) => { return req.headers.get('X-Disable-Signed-URL') !== 'false' }, }, }, }, }) ```
1 parent 4ac1894 commit 865a9cd

File tree

5 files changed

+61
-11
lines changed

5 files changed

+61
-11
lines changed

docs/upload/storage-adapters.mdx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ pnpm add @payloadcms/storage-s3
8484
- The `config` object can be any [`S3ClientConfig`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3) object (from [`@aws-sdk/client-s3`](https://github.com/aws/aws-sdk-js-v3)). _This is highly dependent on your AWS setup_. Check the AWS documentation for more information.
8585
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
8686
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method for the bucket to your website.
87-
- Configure `signedDownloads` (either globally of per-collection in `collections`) to use [presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html) for files downloading. This can improve performance for large files (like videos) while still respecting your access control.
87+
- Configure `signedDownloads` (either globally of per-collection in `collections`) to use [presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html) for files downloading. This can improve performance for large files (like videos) while still respecting your access control. Additionally, with `signedDownloads.shouldUseSignedURL` you can specify a condition whether Payload should use a presigned URL, if you want to use this feature only for specific files.
8888

8989
```ts
9090
import { s3Storage } from '@payloadcms/storage-s3'
@@ -100,6 +100,14 @@ export default buildConfig({
100100
'media-with-prefix': {
101101
prefix,
102102
},
103+
'media-with-presigned-downloads': {
104+
// Filter only mp4 files
105+
signedDownloads: {
106+
shouldUseSignedURL: ({ collection, filename, req }) => {
107+
return filename.endsWith('.mp4')
108+
},
109+
},
110+
},
103111
},
104112
bucket: process.env.S3_BUCKET,
105113
config: {

packages/storage-s3/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pnpm add @payloadcms/storage-s3
1616
- The `config` object can be any [`S3ClientConfig`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3) object (from [`@aws-sdk/client-s3`](https://github.com/aws/aws-sdk-js-v3)). _This is highly dependent on your AWS setup_. Check the AWS documentation for more information.
1717
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
1818
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method for the bucket to your website.
19+
- Configure `signedDownloads` (either globally of per-collection in `collections`) to use [presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html) for files downloading. This can improve performance for large files (like videos) while still respecting your access control. Additionally, with `signedDownloads.shouldUseSignedURL` you can specify a condition whether Payload should use a presigned URL, if you want to use this feature only for specific files.
1920

2021
```ts
2122
import { s3Storage } from '@payloadcms/storage-s3'
@@ -31,6 +32,14 @@ export default buildConfig({
3132
'media-with-prefix': {
3233
prefix,
3334
},
35+
'media-with-presigned-downloads': {
36+
// Filter only mp4 files
37+
signedDownloads: {
38+
shouldUseSignedURL: ({ collection, filename, req }) => {
39+
return filename.endsWith('.mp4')
40+
},
41+
},
42+
},
3443
},
3544
bucket: process.env.S3_BUCKET,
3645
config: {

packages/storage-s3/src/staticHandler.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type * as AWS from '@aws-sdk/client-s3'
22
import type { StaticHandler } from '@payloadcms/plugin-cloud-storage/types'
3-
import type { CollectionConfig } from 'payload'
3+
import type { CollectionConfig, PayloadRequest } from 'payload'
44
import type { Readable } from 'stream'
55

66
import { GetObjectCommand } from '@aws-sdk/client-s3'
@@ -12,6 +12,11 @@ export type SignedDownloadsConfig =
1212
| {
1313
/** @default 7200 */
1414
expiresIn?: number
15+
shouldUseSignedURL?(args: {
16+
collection: CollectionConfig
17+
filename: string
18+
req: PayloadRequest
19+
}): boolean | Promise<boolean>
1520
}
1621
| boolean
1722

@@ -64,14 +69,24 @@ export const getHandler = ({
6469
const key = path.posix.join(prefix, filename)
6570

6671
if (signedDownloads && !clientUploadContext) {
67-
const command = new GetObjectCommand({ Bucket: bucket, Key: key })
68-
const signedUrl = await getSignedUrl(
69-
// @ts-expect-error mismatch versions
70-
getStorageClient(),
71-
command,
72-
typeof signedDownloads === 'object' ? signedDownloads : { expiresIn: 7200 },
73-
)
74-
return Response.redirect(signedUrl)
72+
let useSignedURL = true
73+
if (
74+
typeof signedDownloads === 'object' &&
75+
typeof signedDownloads.shouldUseSignedURL === 'function'
76+
) {
77+
useSignedURL = await signedDownloads.shouldUseSignedURL({ collection, filename, req })
78+
}
79+
80+
if (useSignedURL) {
81+
const command = new GetObjectCommand({ Bucket: bucket, Key: key })
82+
const signedUrl = await getSignedUrl(
83+
// @ts-expect-error mismatch versions
84+
getStorageClient(),
85+
command,
86+
typeof signedDownloads === 'object' ? signedDownloads : { expiresIn: 7200 },
87+
)
88+
return Response.redirect(signedUrl)
89+
}
7590
}
7691

7792
object = await getStorageClient().getObject({

test/storage-s3/config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ export default buildConfigWithDefaults({
4444
prefix,
4545
},
4646
[mediaWithSignedDownloadsSlug]: {
47-
signedDownloads: true,
47+
signedDownloads: {
48+
shouldUseSignedURL: (args) => {
49+
return args.req.headers.get('X-Disable-Signed-URL') !== 'true'
50+
},
51+
},
4852
},
4953
},
5054
bucket: process.env.S3_BUCKET,

test/storage-s3/int.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,20 @@ describe('@payloadcms/storage-s3', () => {
9696
expect(file.headers.get('Content-Type')).toBe('image/png')
9797
})
9898

99+
it('should skip signed download', async () => {
100+
await payload.create({
101+
collection: mediaWithSignedDownloadsSlug,
102+
data: {},
103+
filePath: path.resolve(dirname, '../uploads/small.png'),
104+
})
105+
106+
const response = await restClient.GET(`/${mediaWithSignedDownloadsSlug}/file/small.png`, {
107+
headers: { 'X-Disable-Signed-URL': 'true' },
108+
})
109+
expect(response.status).toBe(200)
110+
expect(response.headers.get('Content-Type')).toBe('image/png')
111+
})
112+
99113
describe('R2', () => {
100114
it.todo('can upload')
101115
})

0 commit comments

Comments
 (0)