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>
112 lines
3.1 KiB
TypeScript
112 lines
3.1 KiB
TypeScript
import type { Stats } from 'fs'
|
|
|
|
import { fileTypeFromFile } from 'file-type'
|
|
import fsPromises from 'fs/promises'
|
|
import { status as httpStatus } from 'http-status'
|
|
import path from 'path'
|
|
|
|
import type { PayloadHandler } from '../../config/types.js'
|
|
|
|
import { APIError } from '../../errors/APIError.js'
|
|
import { checkFileAccess } from '../../uploads/checkFileAccess.js'
|
|
import { streamFile } from '../../uploads/fetchAPI-stream-file/index.js'
|
|
import { getFileTypeFallback } from '../../uploads/getFileTypeFallback.js'
|
|
import { getRequestCollection } from '../../utilities/getRequestEntity.js'
|
|
import { headersWithCors } from '../../utilities/headersWithCors.js'
|
|
|
|
export const getFileHandler: PayloadHandler = async (req) => {
|
|
const collection = getRequestCollection(req)
|
|
|
|
const filename = req.routeParams?.filename as string
|
|
|
|
if (!collection.config.upload) {
|
|
throw new APIError(
|
|
`This collection is not an upload collection: ${collection.config.slug}`,
|
|
httpStatus.BAD_REQUEST,
|
|
)
|
|
}
|
|
|
|
const accessResult = (await checkFileAccess({
|
|
collection,
|
|
filename,
|
|
req,
|
|
}))!
|
|
|
|
if (accessResult instanceof Response) {
|
|
return accessResult
|
|
}
|
|
|
|
if (collection.config.upload.handlers?.length) {
|
|
let customResponse: null | Response | void = null
|
|
const headers = new Headers()
|
|
|
|
for (const handler of collection.config.upload.handlers) {
|
|
customResponse = await handler(req, {
|
|
doc: accessResult,
|
|
headers,
|
|
params: {
|
|
collection: collection.config.slug,
|
|
filename,
|
|
},
|
|
})
|
|
}
|
|
|
|
if (customResponse instanceof Response) {
|
|
return customResponse
|
|
}
|
|
}
|
|
|
|
const fileDir = collection.config.upload?.staticDir || collection.config.slug
|
|
const filePath = path.resolve(`${fileDir}/${filename}`)
|
|
let stats: Stats
|
|
|
|
try {
|
|
stats = await fsPromises.stat(filePath)
|
|
} catch (err) {
|
|
if ((err as { code?: string }).code === 'ENOENT') {
|
|
req.payload.logger.error(
|
|
`File ${filename} for collection ${collection.config.slug} is missing on the disk. Expected path: ${filePath}`,
|
|
)
|
|
|
|
// Omit going to the routeError handler by returning response instead of
|
|
// throwing an error to cut down log noise. The response still matches what you get with APIError to not leak details to the user.
|
|
return Response.json(
|
|
{
|
|
errors: [
|
|
{
|
|
message: 'Something went wrong.',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
headers: headersWithCors({
|
|
headers: new Headers(),
|
|
req,
|
|
}),
|
|
status: 500,
|
|
},
|
|
)
|
|
}
|
|
|
|
throw err
|
|
}
|
|
|
|
const data = streamFile(filePath)
|
|
const fileTypeResult = (await fileTypeFromFile(filePath)) || getFileTypeFallback(filePath)
|
|
|
|
let headers = new Headers()
|
|
headers.set('Content-Type', fileTypeResult.mime)
|
|
headers.set('Content-Length', stats.size + '')
|
|
headers = collection.config.upload?.modifyResponseHeaders
|
|
? collection.config.upload.modifyResponseHeaders({ headers }) || headers
|
|
: headers
|
|
|
|
return new Response(data, {
|
|
headers: headersWithCors({
|
|
headers,
|
|
req,
|
|
}),
|
|
status: httpStatus.OK,
|
|
})
|
|
}
|