diff --git a/packages/storage-vercel-blob/src/client/VercelBlobClientUploadHandler.ts b/packages/storage-vercel-blob/src/client/VercelBlobClientUploadHandler.ts index 002939e1dfe..3740d3a27de 100644 --- a/packages/storage-vercel-blob/src/client/VercelBlobClientUploadHandler.ts +++ b/packages/storage-vercel-blob/src/client/VercelBlobClientUploadHandler.ts @@ -5,6 +5,7 @@ import { formatAdminURL } from 'payload/shared' export type VercelBlobClientUploadHandlerExtra = { addRandomSuffix: boolean + allowOverwrite: boolean baseURL: string prefix: string } @@ -14,7 +15,7 @@ export const VercelBlobClientUploadHandler = handler: async ({ apiRoute, collectionSlug, - extra: { addRandomSuffix, baseURL, prefix = '' }, + extra: { addRandomSuffix, allowOverwrite, baseURL, prefix = '' }, file, serverHandlerPath, serverURL, diff --git a/packages/storage-vercel-blob/src/getClientUploadRoute.ts b/packages/storage-vercel-blob/src/getClientUploadRoute.ts index 0dc2d6434f6..d17b8202a51 100644 --- a/packages/storage-vercel-blob/src/getClientUploadRoute.ts +++ b/packages/storage-vercel-blob/src/getClientUploadRoute.ts @@ -9,6 +9,7 @@ type Args = { req: PayloadRequest }) => boolean | Promise addRandomSuffix?: boolean + allowOverwrite?: boolean cacheControlMaxAge?: number token: string } @@ -16,7 +17,7 @@ type Args = { const defaultAccess: Args['access'] = ({ req }) => !!req.user export const getClientUploadRoute = - ({ access = defaultAccess, addRandomSuffix, cacheControlMaxAge, token }: Args): PayloadHandler => + ({ access = defaultAccess, addRandomSuffix, allowOverwrite, cacheControlMaxAge, token }: Args): PayloadHandler => async (req) => { const body = (await req.json!()) as HandleUploadBody @@ -34,6 +35,7 @@ export const getClientUploadRoute = return Promise.resolve({ addRandomSuffix, + allowOverwrite, cacheControlMaxAge, }) }, diff --git a/packages/storage-vercel-blob/src/handleUpload.ts b/packages/storage-vercel-blob/src/handleUpload.ts index 4bf4f53f2ef..8be2366a40f 100644 --- a/packages/storage-vercel-blob/src/handleUpload.ts +++ b/packages/storage-vercel-blob/src/handleUpload.ts @@ -13,6 +13,7 @@ type HandleUploadArgs = { export const getHandleUpload = ({ access = 'public', addRandomSuffix, + allowOverwrite, baseUrl, cacheControlMaxAge, prefix = '', @@ -24,6 +25,7 @@ export const getHandleUpload = ({ const result = await put(fileKey, buffer, { access, addRandomSuffix, + allowOverwrite, cacheControlMaxAge, contentType: mimeType, token, diff --git a/packages/storage-vercel-blob/src/index.ts b/packages/storage-vercel-blob/src/index.ts index 65f83d8fd4d..bce58ec5484 100644 --- a/packages/storage-vercel-blob/src/index.ts +++ b/packages/storage-vercel-blob/src/index.ts @@ -34,6 +34,21 @@ export type VercelBlobStorageOptions = { */ addRandomSuffix?: boolean + /** + * Allow overwriting existing blobs with the same pathname. + * + * When `false` (the default), uploading a file with the same name as an + * existing blob will throw an error. This commonly happens when a previous + * upload fails partway through (the blob is created but the Payload record + * is not), leaving an orphan blob that blocks subsequent uploads of the + * same file. + * + * Set to `true` to silently overwrite the existing blob instead of erroring. + * + * @default false + */ + allowOverwrite?: boolean + /** * When enabled, fields (like the prefix field) will always be inserted into * the collection schema regardless of whether the plugin is enabled. This @@ -82,6 +97,7 @@ export type VercelBlobStorageOptions = { const defaultUploadOptions: Partial = { access: 'public', addRandomSuffix: false, + allowOverwrite: false, cacheControlMaxAge: 60 * 60 * 24 * 365, // 1 year enabled: true, } @@ -122,6 +138,7 @@ export const vercelBlobStorage: VercelBlobStoragePlugin = enabled: !isPluginDisabled && Boolean(options.clientUploads), extraClientHandlerProps: (collection) => ({ addRandomSuffix: !!optionsWithDefaults.addRandomSuffix, + allowOverwrite: !!optionsWithDefaults.allowOverwrite, baseURL: baseUrl, prefix: (typeof collection === 'object' && collection.prefix && `${collection.prefix}/`) || '', @@ -130,6 +147,7 @@ export const vercelBlobStorage: VercelBlobStoragePlugin = access: typeof options.clientUploads === 'object' ? options.clientUploads.access : undefined, addRandomSuffix: optionsWithDefaults.addRandomSuffix, + allowOverwrite: optionsWithDefaults.allowOverwrite, cacheControlMaxAge: options.cacheControlMaxAge, token: options.token ?? '', }), @@ -185,7 +203,7 @@ function vercelBlobStorageInternal( options: { baseUrl: string } & VercelBlobStorageOptions, ): Adapter { return ({ collection, prefix }): GeneratedAdapter => { - const { access, addRandomSuffix, baseUrl, cacheControlMaxAge, clientUploads, token } = options + const { access, addRandomSuffix, allowOverwrite, baseUrl, cacheControlMaxAge, clientUploads, token } = options if (!token) { throw new Error('Vercel Blob storage token is required') @@ -199,6 +217,7 @@ function vercelBlobStorageInternal( handleUpload: getHandleUpload({ access, addRandomSuffix, + allowOverwrite, baseUrl, cacheControlMaxAge, prefix,