Files
payload/packages/storage-uploadthing/src/staticHandler.ts
Sasha b540da53ec feat(storage-*): large file uploads on Vercel (#11382)
Currently, usage of Payload on Vercel has a limitation - uploads are
limited by 4.5MB file size.
This PR allows you to pass `clientUploads: true` to all existing storage
adapters
* Storage S3
* Vercel Blob
* Google Cloud Storage
* Uploadthing
* Azure Blob

And then, Payload will do uploads on the client instead. With the S3
Adapter it uses signed URLs and with Vercel Blob it does this -
https://vercel.com/guides/how-to-bypass-vercel-body-size-limit-serverless-functions#step-2:-create-a-client-upload-route.
Note that it doesn't mean that anyone can now upload files to your
storage, it still does auth checks and you can customize that with
`clientUploads.access`


https://github.com/user-attachments/assets/5083c76c-8f5a-43dc-a88c-9ddc4527d91c

Implements https://github.com/payloadcms/payload/discussions/7569
feature request.
2025-02-26 21:59:34 +02:00

110 lines
2.9 KiB
TypeScript

import type { StaticHandler } from '@payloadcms/plugin-cloud-storage/types'
import type { Where } from 'payload'
import type { UTApi } from 'uploadthing/server'
import { getKeyFromFilename } from './utilities.js'
type Args = {
utApi: UTApi
}
export const getHandler = ({ utApi }: Args): StaticHandler => {
return async (req, { doc, params: { clientUploadContext, collection, filename } }) => {
try {
let key: string
if (
clientUploadContext &&
typeof clientUploadContext === 'object' &&
'key' in clientUploadContext &&
typeof clientUploadContext.key === 'string'
) {
key = clientUploadContext.key
} else {
const collectionConfig = req.payload.collections[collection]?.config
let retrievedDoc = doc
if (!retrievedDoc) {
const or: Where[] = [
{
filename: {
equals: filename,
},
},
]
if (collectionConfig.upload.imageSizes) {
collectionConfig.upload.imageSizes.forEach(({ name }) => {
or.push({
[`sizes.${name}.filename`]: {
equals: filename,
},
})
})
}
const result = await req.payload.db.findOne({
collection,
req,
where: { or },
})
if (result) {
retrievedDoc = result
}
}
if (!retrievedDoc) {
return new Response(null, { status: 404, statusText: 'Not Found' })
}
key = getKeyFromFilename(retrievedDoc, filename)
}
if (!key) {
return new Response(null, { status: 404, statusText: 'Not Found' })
}
const { url: signedURL } = await utApi.getSignedURL(key)
if (!signedURL) {
return new Response(null, { status: 404, statusText: 'Not Found' })
}
const response = await fetch(signedURL)
if (!response.ok) {
return new Response(null, { status: 404, statusText: 'Not Found' })
}
const blob = await response.blob()
const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')
const objectEtag = response.headers.get('etag')
if (etagFromHeaders && etagFromHeaders === objectEtag) {
return new Response(null, {
headers: new Headers({
'Content-Length': String(blob.size),
'Content-Type': blob.type,
ETag: objectEtag,
}),
status: 304,
})
}
return new Response(blob, {
headers: new Headers({
'Content-Length': String(blob.size),
'Content-Type': blob.type,
ETag: objectEtag,
}),
status: 200,
})
} catch (err) {
req.payload.logger.error({ err, msg: 'Unexpected error in staticHandler' })
return new Response('Internal Server Error', { status: 500 })
}
}
}