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.
157 lines
4.5 KiB
TypeScript
157 lines
4.5 KiB
TypeScript
import type {
|
|
Adapter,
|
|
ClientUploadsConfig,
|
|
PluginOptions as CloudStoragePluginOptions,
|
|
CollectionOptions,
|
|
GeneratedAdapter,
|
|
} from '@payloadcms/plugin-cloud-storage/types'
|
|
import type { Config, Field, Plugin, UploadCollectionSlug } from 'payload'
|
|
import type { UTApiOptions } from 'uploadthing/types'
|
|
|
|
import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage'
|
|
import { initClientUploads } from '@payloadcms/plugin-cloud-storage/utilities'
|
|
import { createRouteHandler } from 'uploadthing/next'
|
|
import { createUploadthing, UTApi } from 'uploadthing/server'
|
|
|
|
import { generateURL } from './generateURL.js'
|
|
import { getClientUploadRoute } from './getClientUploadRoute.js'
|
|
import { getHandleDelete } from './handleDelete.js'
|
|
import { getHandleUpload } from './handleUpload.js'
|
|
import { getHandler } from './staticHandler.js'
|
|
|
|
export type UploadthingStorageOptions = {
|
|
/**
|
|
* Do uploads directly on the client, to bypass limits on Vercel.
|
|
*/
|
|
clientUploads?: ClientUploadsConfig
|
|
|
|
/**
|
|
* Collection options to apply the adapter to.
|
|
*/
|
|
collections: Partial<Record<UploadCollectionSlug, Omit<CollectionOptions, 'adapter'> | true>>
|
|
|
|
/**
|
|
* Whether or not to enable the plugin
|
|
*
|
|
* Default: true
|
|
*/
|
|
enabled?: boolean
|
|
|
|
/**
|
|
* Uploadthing Options
|
|
*/
|
|
options: {
|
|
/**
|
|
* @default 'public-read'
|
|
*/
|
|
acl?: ACL
|
|
} & UTApiOptions
|
|
}
|
|
|
|
type UploadthingPlugin = (uploadthingStorageOptions: UploadthingStorageOptions) => Plugin
|
|
|
|
/** NOTE: not synced with uploadthing's internal types. Need to modify if more options added */
|
|
export type ACL = 'private' | 'public-read'
|
|
|
|
export const uploadthingStorage: UploadthingPlugin =
|
|
(uploadthingStorageOptions: UploadthingStorageOptions) =>
|
|
(incomingConfig: Config): Config => {
|
|
if (uploadthingStorageOptions.enabled === false) {
|
|
return incomingConfig
|
|
}
|
|
|
|
// Default ACL to public-read
|
|
if (!uploadthingStorageOptions.options.acl) {
|
|
uploadthingStorageOptions.options.acl = 'public-read'
|
|
}
|
|
|
|
const adapter = uploadthingInternal(uploadthingStorageOptions)
|
|
|
|
initClientUploads({
|
|
clientHandler: '@payloadcms/storage-uploadthing/client#UploadthingClientUploadHandler',
|
|
collections: uploadthingStorageOptions.collections,
|
|
config: incomingConfig,
|
|
enabled: !!uploadthingStorageOptions.clientUploads,
|
|
serverHandler: getClientUploadRoute({
|
|
access:
|
|
typeof uploadthingStorageOptions.clientUploads === 'object'
|
|
? uploadthingStorageOptions.clientUploads.access
|
|
: undefined,
|
|
acl: uploadthingStorageOptions.options.acl || 'public-read',
|
|
token: uploadthingStorageOptions.options.token,
|
|
}),
|
|
serverHandlerPath: '/storage-uploadthing-client-upload-route',
|
|
})
|
|
|
|
// Add adapter to each collection option object
|
|
const collectionsWithAdapter: CloudStoragePluginOptions['collections'] = Object.entries(
|
|
uploadthingStorageOptions.collections,
|
|
).reduce(
|
|
(acc, [slug, collOptions]) => ({
|
|
...acc,
|
|
[slug]: {
|
|
...(collOptions === true ? {} : collOptions),
|
|
|
|
// Disable payload access control if the ACL is public-read or not set
|
|
// ...(uploadthingStorageOptions.options.acl === 'public-read'
|
|
// ? { disablePayloadAccessControl: true }
|
|
// : {}),
|
|
|
|
adapter,
|
|
},
|
|
}),
|
|
{} as Record<string, CollectionOptions>,
|
|
)
|
|
|
|
// Set disableLocalStorage: true for collections specified in the plugin options
|
|
const config = {
|
|
...incomingConfig,
|
|
collections: (incomingConfig.collections || []).map((collection) => {
|
|
if (!collectionsWithAdapter[collection.slug]) {
|
|
return collection
|
|
}
|
|
|
|
return {
|
|
...collection,
|
|
upload: {
|
|
...(typeof collection.upload === 'object' ? collection.upload : {}),
|
|
disableLocalStorage: true,
|
|
},
|
|
}
|
|
}),
|
|
}
|
|
|
|
return cloudStoragePlugin({
|
|
collections: collectionsWithAdapter,
|
|
})(config)
|
|
}
|
|
|
|
function uploadthingInternal(options: UploadthingStorageOptions): Adapter {
|
|
const fields: Field[] = [
|
|
{
|
|
name: '_key',
|
|
type: 'text',
|
|
admin: {
|
|
hidden: true,
|
|
},
|
|
},
|
|
]
|
|
|
|
return (): GeneratedAdapter => {
|
|
const {
|
|
options: { acl = 'public-read', ...utOptions },
|
|
} = options
|
|
|
|
const utApi = new UTApi(utOptions)
|
|
|
|
return {
|
|
name: 'uploadthing',
|
|
fields,
|
|
generateURL,
|
|
handleDelete: getHandleDelete({ utApi }),
|
|
handleUpload: getHandleUpload({ acl, utApi }),
|
|
staticHandler: getHandler({ utApi }),
|
|
}
|
|
}
|
|
}
|