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.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
'use client'
|
||||
import { createClientUploadHandler } from '@payloadcms/plugin-cloud-storage/client'
|
||||
import { upload } from '@vercel/blob/client'
|
||||
|
||||
export type VercelBlobClientUploadHandlerExtra = {
|
||||
addRandomSuffix: boolean
|
||||
baseURL: string
|
||||
prefix: string
|
||||
}
|
||||
|
||||
export const VercelBlobClientUploadHandler =
|
||||
createClientUploadHandler<VercelBlobClientUploadHandlerExtra>({
|
||||
handler: async ({
|
||||
apiRoute,
|
||||
collectionSlug,
|
||||
extra: { addRandomSuffix, baseURL, prefix = '' },
|
||||
file,
|
||||
serverHandlerPath,
|
||||
serverURL,
|
||||
updateFilename,
|
||||
}) => {
|
||||
const result = await upload(`${prefix}${file.name}`, file, {
|
||||
access: 'public',
|
||||
clientPayload: collectionSlug,
|
||||
contentType: file.type,
|
||||
handleUploadUrl: `${serverURL}${apiRoute}${serverHandlerPath}`,
|
||||
})
|
||||
|
||||
// Update filename with suffix from returned url
|
||||
if (addRandomSuffix) {
|
||||
updateFilename(result.url.replace(`${baseURL}/`, ''))
|
||||
}
|
||||
},
|
||||
})
|
||||
1
packages/storage-vercel-blob/src/exports/client.ts
Normal file
1
packages/storage-vercel-blob/src/exports/client.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { VercelBlobClientUploadHandler } from '../client/VercelBlobClientUploadHandler.js'
|
||||
50
packages/storage-vercel-blob/src/getClientUploadRoute.ts
Normal file
50
packages/storage-vercel-blob/src/getClientUploadRoute.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { PayloadHandler, PayloadRequest, UploadCollectionSlug } from 'payload'
|
||||
|
||||
import { handleUpload, type HandleUploadBody } from '@vercel/blob/client'
|
||||
import { APIError, Forbidden } from 'payload'
|
||||
|
||||
type Args = {
|
||||
access?: (args: {
|
||||
collectionSlug: UploadCollectionSlug
|
||||
req: PayloadRequest
|
||||
}) => boolean | Promise<boolean>
|
||||
addRandomSuffix?: boolean
|
||||
cacheControlMaxAge?: number
|
||||
token: string
|
||||
}
|
||||
|
||||
const defaultAccess: Args['access'] = ({ req }) => !!req.user
|
||||
|
||||
export const getClientUploadRoute =
|
||||
({ access = defaultAccess, addRandomSuffix, cacheControlMaxAge, token }: Args): PayloadHandler =>
|
||||
async (req) => {
|
||||
const body = (await req.json!()) as HandleUploadBody
|
||||
|
||||
try {
|
||||
const jsonResponse = await handleUpload({
|
||||
body,
|
||||
onBeforeGenerateToken: async (_pathname: string, collectionSlug: null | string) => {
|
||||
if (!collectionSlug) {
|
||||
throw new APIError('No payload was provided')
|
||||
}
|
||||
|
||||
if (!(await access({ collectionSlug, req }))) {
|
||||
throw new Forbidden()
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
addRandomSuffix,
|
||||
cacheControlMaxAge,
|
||||
})
|
||||
},
|
||||
onUploadCompleted: async () => {},
|
||||
request: req as Request,
|
||||
token,
|
||||
})
|
||||
|
||||
return Response.json(jsonResponse)
|
||||
} catch (error) {
|
||||
req.payload.logger.error(error)
|
||||
throw new APIError('storage-vercel-blob client upload route error')
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
Adapter,
|
||||
ClientUploadsConfig,
|
||||
PluginOptions as CloudStoragePluginOptions,
|
||||
CollectionOptions,
|
||||
GeneratedAdapter,
|
||||
@@ -7,8 +8,12 @@ import type {
|
||||
import type { Config, Plugin, UploadCollectionSlug } from 'payload'
|
||||
|
||||
import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage'
|
||||
import { initClientUploads } from '@payloadcms/plugin-cloud-storage/utilities'
|
||||
|
||||
import type { VercelBlobClientUploadHandlerExtra } from './client/VercelBlobClientUploadHandler.js'
|
||||
|
||||
import { getGenerateUrl } from './generateURL.js'
|
||||
import { getClientUploadRoute } from './getClientUploadRoute.js'
|
||||
import { getHandleDelete } from './handleDelete.js'
|
||||
import { getHandleUpload } from './handleUpload.js'
|
||||
import { getStaticHandler } from './staticHandler.js'
|
||||
@@ -32,10 +37,15 @@ export type VercelBlobStorageOptions = {
|
||||
/**
|
||||
* Cache-Control max-age in seconds
|
||||
*
|
||||
* @defaultvalue 365 * 24 * 60 * 60 (1 Year)
|
||||
* @default 365 * 24 * 60 * 60 // (1 Year)
|
||||
*/
|
||||
cacheControlMaxAge?: number
|
||||
|
||||
/**
|
||||
* Do uploads directly on the client, to bypass limits on Vercel.
|
||||
*/
|
||||
clientUploads?: ClientUploadsConfig
|
||||
|
||||
/**
|
||||
* Collections to apply the Vercel Blob adapter to
|
||||
*/
|
||||
@@ -91,6 +101,29 @@ export const vercelBlobStorage: VercelBlobStoragePlugin =
|
||||
|
||||
const baseUrl = `https://${storeId}.${optionsWithDefaults.access}.blob.vercel-storage.com`
|
||||
|
||||
initClientUploads<
|
||||
VercelBlobClientUploadHandlerExtra,
|
||||
VercelBlobStorageOptions['collections'][string]
|
||||
>({
|
||||
clientHandler: '@payloadcms/storage-vercel-blob/client#VercelBlobClientUploadHandler',
|
||||
collections: options.collections,
|
||||
config: incomingConfig,
|
||||
enabled: !!options.clientUploads,
|
||||
extraClientHandlerProps: (collection) => ({
|
||||
addRandomSuffix: !!optionsWithDefaults.addRandomSuffix,
|
||||
baseURL: baseUrl,
|
||||
prefix: (typeof collection === 'object' && collection.prefix) || '',
|
||||
}),
|
||||
serverHandler: getClientUploadRoute({
|
||||
access:
|
||||
typeof options.clientUploads === 'object' ? options.clientUploads.access : undefined,
|
||||
addRandomSuffix: optionsWithDefaults.addRandomSuffix,
|
||||
cacheControlMaxAge: options.cacheControlMaxAge,
|
||||
token: options.token,
|
||||
}),
|
||||
serverHandlerPath: '/vercel-blob-client-upload-route',
|
||||
})
|
||||
|
||||
const adapter = vercelBlobStorageInternal({ ...optionsWithDefaults, baseUrl })
|
||||
|
||||
// Add adapter to each collection option object
|
||||
|
||||
@@ -22,7 +22,6 @@ export const getStaticHandler = (
|
||||
|
||||
const fileUrl = `${baseUrl}/${fileKey}`
|
||||
const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')
|
||||
|
||||
const blobMetadata = await head(fileUrl, { token })
|
||||
const uploadedAtString = blobMetadata.uploadedAt.toISOString()
|
||||
const ETag = `"${fileKey}-${uploadedAtString}"`
|
||||
|
||||
Reference in New Issue
Block a user