Files
payload/packages/storage-uploadthing/src/index.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

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 }),
}
}
}