This PR makes it so that `modifyResponseHeaders` is supported in our
adapters when set on the collection config. Previously it would be
ignored.
This means that users can now modify or append new headers to what's
returned by each service.
```ts
import type { CollectionConfig } from 'payload'
export const Media: CollectionConfig = {
slug: 'media',
upload: {
modifyResponseHeaders: ({ headers }) => {
const newHeaders = new Headers(headers) // Copy existing headers
newHeaders.set('X-Frame-Options', 'DENY') // Set new header
return newHeaders
},
},
}
```
Also adds support for `void` return on the `modifyResponseHeaders`
function in the case where the user just wants to use existing headers
and doesn't need more control.
eg:
```ts
import type { CollectionConfig } from 'payload'
export const Media: CollectionConfig = {
slug: 'media',
upload: {
modifyResponseHeaders: ({ headers }) => {
headers.set('X-Frame-Options', 'DENY') // You can directly set headers without returning
},
},
}
```
Manual testing checklist (no CI e2es setup for these envs yet):
- [x] GCS
- [x] S3
- [x] Azure
- [x] UploadThing
- [x] Vercel Blob
---------
Co-authored-by: James <james@trbl.design>
98 lines
3.0 KiB
TypeScript
98 lines
3.0 KiB
TypeScript
import type { ContainerClient } from '@azure/storage-blob'
|
|
import type { StaticHandler } from '@payloadcms/plugin-cloud-storage/types'
|
|
import type { CollectionConfig } from 'payload'
|
|
|
|
import { RestError } from '@azure/storage-blob'
|
|
import { getFilePrefix } from '@payloadcms/plugin-cloud-storage/utilities'
|
|
import path from 'path'
|
|
|
|
import { getRangeFromHeader } from './utils/getRangeFromHeader.js'
|
|
|
|
interface Args {
|
|
collection: CollectionConfig
|
|
getStorageClient: () => ContainerClient
|
|
}
|
|
|
|
export const getHandler = ({ collection, getStorageClient }: Args): StaticHandler => {
|
|
return async (req, { headers: incomingHeaders, params: { clientUploadContext, filename } }) => {
|
|
try {
|
|
const prefix = await getFilePrefix({ clientUploadContext, collection, filename, req })
|
|
const blockBlobClient = getStorageClient().getBlockBlobClient(
|
|
path.posix.join(prefix, filename),
|
|
)
|
|
|
|
const { end, start } = await getRangeFromHeader(
|
|
blockBlobClient,
|
|
String(req.headers.get('range')),
|
|
)
|
|
|
|
const blob = await blockBlobClient.download(start, end)
|
|
|
|
const response = blob._response
|
|
|
|
let initHeaders: Headers = {
|
|
...(response.headers.rawHeaders() as unknown as Headers),
|
|
}
|
|
|
|
// Typescript is difficult here with merging these types from Azure
|
|
if (incomingHeaders) {
|
|
initHeaders = {
|
|
...initHeaders,
|
|
...incomingHeaders,
|
|
}
|
|
}
|
|
|
|
let headers = new Headers(initHeaders)
|
|
|
|
const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')
|
|
const objectEtag = response.headers.get('etag')
|
|
|
|
if (
|
|
collection.upload &&
|
|
typeof collection.upload === 'object' &&
|
|
typeof collection.upload.modifyResponseHeaders === 'function'
|
|
) {
|
|
headers = collection.upload.modifyResponseHeaders({ headers }) || headers
|
|
}
|
|
|
|
if (etagFromHeaders && etagFromHeaders === objectEtag) {
|
|
return new Response(null, {
|
|
headers,
|
|
status: 304,
|
|
})
|
|
}
|
|
|
|
// Manually create a ReadableStream for the web from a Node.js stream.
|
|
const readableStream = new ReadableStream({
|
|
start(controller) {
|
|
const nodeStream = blob.readableStreamBody
|
|
if (!nodeStream) {
|
|
throw new Error('No readable stream body')
|
|
}
|
|
|
|
nodeStream.on('data', (chunk) => {
|
|
controller.enqueue(new Uint8Array(chunk))
|
|
})
|
|
nodeStream.on('end', () => {
|
|
controller.close()
|
|
})
|
|
nodeStream.on('error', (err) => {
|
|
controller.error(err)
|
|
})
|
|
},
|
|
})
|
|
|
|
return new Response(readableStream, {
|
|
headers,
|
|
status: response.status,
|
|
})
|
|
} catch (err: unknown) {
|
|
if (err instanceof RestError && err.statusCode === 404) {
|
|
return new Response(null, { status: 404, statusText: 'Not Found' })
|
|
}
|
|
req.payload.logger.error(err)
|
|
return new Response('Internal Server Error', { status: 500 })
|
|
}
|
|
}
|
|
}
|