From b540da53ec34ef823da0ccde10c0d196a6277409 Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Wed, 26 Feb 2025 21:59:34 +0200 Subject: [PATCH] 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. --- docs/upload/storage-adapters.mdx | 37 +- packages/payload/src/uploads/types.ts | 2 +- .../src/utilities/addDataAndFileToRequest.ts | 48 ++ packages/plugin-cloud-storage/.swcrc | 9 + packages/plugin-cloud-storage/package.json | 17 +- .../src/client/createClientUploadHandler.tsx | 69 +++ .../src/exports/client.ts | 1 + .../src/exports/utilities.ts | 1 + packages/plugin-cloud-storage/src/types.ts | 16 +- .../src/utilities/initClientUploads.ts | 76 ++++ packages/plugin-cloud-storage/tsconfig.json | 2 +- packages/storage-azure/README.md | 2 + packages/storage-azure/package.json | 10 + .../src/client/AzureClientUploadHandler.ts | 29 ++ packages/storage-azure/src/exports/client.ts | 1 + .../storage-azure/src/generateSignedURL.ts | 62 +++ packages/storage-azure/src/index.ts | 46 +- packages/storage-gcs/README.md | 1 + packages/storage-gcs/package.json | 10 + .../src/client/GcsClientUploadHandler.ts | 24 + packages/storage-gcs/src/exports/client.ts | 1 + packages/storage-gcs/src/generateSignedURL.ts | 58 +++ packages/storage-gcs/src/index.ts | 76 +++- packages/storage-s3/README.md | 1 + packages/storage-s3/package.json | 11 + .../src/client/S3ClientUploadHandler.ts | 24 + packages/storage-s3/src/exports/client.ts | 1 + packages/storage-s3/src/generateSignedURL.ts | 59 +++ packages/storage-s3/src/index.ts | 52 ++- packages/storage-uploadthing/README.md | 1 + packages/storage-uploadthing/package.json | 10 + .../client/UploadthingClientUploadHandler.ts | 18 + .../storage-uploadthing/src/exports/client.ts | 1 + .../src/getClientUploadRoute.ts | 62 +++ packages/storage-uploadthing/src/index.ts | 27 +- .../storage-uploadthing/src/staticHandler.ts | 69 +-- packages/storage-vercel-blob/README.md | 2 + packages/storage-vercel-blob/package.json | 10 + .../client/VercelBlobClientUploadHandler.ts | 34 ++ .../storage-vercel-blob/src/exports/client.ts | 1 + .../src/getClientUploadRoute.ts | 50 ++ packages/storage-vercel-blob/src/index.ts | 35 +- .../storage-vercel-blob/src/staticHandler.ts | 1 - .../BulkUpload/FormsManager/createFormData.ts | 33 +- .../BulkUpload/FormsManager/index.tsx | 21 +- packages/ui/src/exports/client/index.ts | 2 + packages/ui/src/forms/Form/index.tsx | 72 ++- packages/ui/src/forms/Form/types.ts | 2 +- packages/ui/src/providers/Root/index.tsx | 5 +- .../ui/src/providers/UploadHandlers/index.tsx | 54 +++ pnpm-lock.yaml | 430 ++++++++++++++++-- test/storage-uploadthing/config.ts | 1 + tsconfig.base.json | 10 +- tsconfig.json | 3 + 54 files changed, 1548 insertions(+), 152 deletions(-) create mode 100644 packages/plugin-cloud-storage/src/client/createClientUploadHandler.tsx create mode 100644 packages/plugin-cloud-storage/src/exports/client.ts create mode 100644 packages/plugin-cloud-storage/src/utilities/initClientUploads.ts create mode 100644 packages/storage-azure/src/client/AzureClientUploadHandler.ts create mode 100644 packages/storage-azure/src/exports/client.ts create mode 100644 packages/storage-azure/src/generateSignedURL.ts create mode 100644 packages/storage-gcs/src/client/GcsClientUploadHandler.ts create mode 100644 packages/storage-gcs/src/exports/client.ts create mode 100644 packages/storage-gcs/src/generateSignedURL.ts create mode 100644 packages/storage-s3/src/client/S3ClientUploadHandler.ts create mode 100644 packages/storage-s3/src/exports/client.ts create mode 100644 packages/storage-s3/src/generateSignedURL.ts create mode 100644 packages/storage-uploadthing/src/client/UploadthingClientUploadHandler.ts create mode 100644 packages/storage-uploadthing/src/exports/client.ts create mode 100644 packages/storage-uploadthing/src/getClientUploadRoute.ts create mode 100644 packages/storage-vercel-blob/src/client/VercelBlobClientUploadHandler.ts create mode 100644 packages/storage-vercel-blob/src/exports/client.ts create mode 100644 packages/storage-vercel-blob/src/getClientUploadRoute.ts create mode 100644 packages/ui/src/providers/UploadHandlers/index.tsx diff --git a/docs/upload/storage-adapters.mdx b/docs/upload/storage-adapters.mdx index 1c7bff85f..af93715e6 100644 --- a/docs/upload/storage-adapters.mdx +++ b/docs/upload/storage-adapters.mdx @@ -30,6 +30,7 @@ pnpm add @payloadcms/storage-vercel-blob - Configure the `collections` object to specify which collections should use the Vercel Blob adapter. The slug _must_ match one of your existing collection slugs. - Ensure you have `BLOB_READ_WRITE_TOKEN` set in your Vercel environment variables. This is usually set by Vercel automatically after adding blob storage to your project. - When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection. +- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. ```ts import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob' @@ -64,6 +65,7 @@ export default buildConfig({ | `addRandomSuffix` | Add a random suffix to the uploaded file name in Vercel Blob storage | `false` | | `cacheControlMaxAge` | Cache-Control max-age in seconds | `365 * 24 * 60 * 60` (1 Year) | | `token` | Vercel Blob storage read/write token | `''` | +| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | | ## S3 Storage [`@payloadcms/storage-s3`](https://www.npmjs.com/package/@payloadcms/storage-s3) @@ -79,6 +81,7 @@ pnpm add @payloadcms/storage-s3 - Configure the `collections` object to specify which collections should use the S3 Storage adapter. The slug _must_ match one of your existing collection slugs. - The `config` object can be any [`S3ClientConfig`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3) object (from [`@aws-sdk/client-s3`](https://github.com/aws/aws-sdk-js-v3)). _This is highly dependent on your AWS setup_. Check the AWS documentation for more information. - When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection. +- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method for the bucket to your website. ```ts import { s3Storage } from '@payloadcms/storage-s3' @@ -126,6 +129,7 @@ pnpm add @payloadcms/storage-azure - Configure the `collections` object to specify which collections should use the Azure Blob adapter. The slug _must_ match one of your existing collection slugs. - When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection. +- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method to your website. ```ts import { azureStorage } from '@payloadcms/storage-azure' @@ -161,6 +165,7 @@ export default buildConfig({ | `baseURL` | Base URL for the Azure Blob storage account | | | `connectionString` | Azure Blob storage connection string | | | `containerName` | Azure Blob storage container name | | +| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | | ## Google Cloud Storage [`@payloadcms/storage-gcs`](https://www.npmjs.com/package/@payloadcms/storage-gcs) @@ -175,6 +180,7 @@ pnpm add @payloadcms/storage-gcs - Configure the `collections` object to specify which collections should use the Google Cloud Storage adapter. The slug _must_ match one of your existing collection slugs. - When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection. +- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method for the bucket to your website. ```ts import { gcsStorage } from '@payloadcms/storage-gcs' @@ -203,13 +209,14 @@ export default buildConfig({ ### Configuration Options#gcs-configuration -| Option | Description | Default | -| ------------- | --------------------------------------------------------------------------------------------------- | --------- | -| `enabled` | Whether or not to enable the plugin | `true` | -| `collections` | Collections to apply the storage to | | -| `bucket` | The name of the bucket to use | | -| `options` | Google Cloud Storage client configuration. See [Docs](https://github.com/googleapis/nodejs-storage) | | -| `acl` | Access control list for files that are uploaded | `Private` | +| Option | Description | Default | +| --------------- | --------------------------------------------------------------------------------------------------- | --------- | +| `enabled` | Whether or not to enable the plugin | `true` | +| `collections` | Collections to apply the storage to | | +| `bucket` | The name of the bucket to use | | +| `options` | Google Cloud Storage client configuration. See [Docs](https://github.com/googleapis/nodejs-storage) | | +| `acl` | Access control list for files that are uploaded | `Private` | +| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | | ## Uploadthing Storage @@ -226,6 +233,7 @@ pnpm add @payloadcms/storage-uploadthing - Configure the `collections` object to specify which collections should use uploadthing. The slug _must_ match one of your existing collection slugs and be an `upload` type. - Get a token from Uploadthing and set it as `token` in the `options` object. - `acl` is optional and defaults to `public-read`. +- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. ```ts export default buildConfig({ @@ -246,13 +254,14 @@ export default buildConfig({ ### Configuration Options#uploadthing-configuration -| Option | Description | Default | -| ---------------- | ----------------------------------------------- | ------------- | -| `token` | Token from Uploadthing. Required. | | -| `acl` | Access control list for files that are uploaded | `public-read` | -| `logLevel` | Log level for Uploadthing | `info` | -| `fetch` | Custom fetch function | `fetch` | -| `defaultKeyType` | Default key type for file operations | `fileKey` | +| Option | Description | Default | +| ---------------- | ------------------------------------------------------------- | ------------- | +| `token` | Token from Uploadthing. Required. | | +| `acl` | Access control list for files that are uploaded | `public-read` | +| `logLevel` | Log level for Uploadthing | `info` | +| `fetch` | Custom fetch function | `fetch` | +| `defaultKeyType` | Default key type for file operations | `fileKey` | +| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | | ## Custom Storage Adapters diff --git a/packages/payload/src/uploads/types.ts b/packages/payload/src/uploads/types.ts index cf24da507..da29c9dd0 100644 --- a/packages/payload/src/uploads/types.ts +++ b/packages/payload/src/uploads/types.ts @@ -178,7 +178,7 @@ export type UploadConfig = { req: PayloadRequest, args: { doc: TypeWithID - params: { collection: string; filename: string } + params: { clientUploadContext?: unknown; collection: string; filename: string } }, ) => Promise | Promise | Response | void)[] /** diff --git a/packages/payload/src/utilities/addDataAndFileToRequest.ts b/packages/payload/src/utilities/addDataAndFileToRequest.ts index c07656953..b08954ccc 100644 --- a/packages/payload/src/utilities/addDataAndFileToRequest.ts +++ b/packages/payload/src/utilities/addDataAndFileToRequest.ts @@ -44,6 +44,54 @@ export const addDataAndFileToRequest: AddDataAndFileToRequest = async (req) => { if (fields?._payload && typeof fields._payload === 'string') { req.data = JSON.parse(fields._payload) } + + if (!req.file && fields?.file && typeof fields?.file === 'string') { + const { clientUploadContext, collectionSlug, filename, mimeType, size } = JSON.parse( + fields.file, + ) + const uploadConfig = req.payload.collections[collectionSlug].config.upload + + if (!uploadConfig.handlers) { + throw new APIError('uploadConfig.handlers is not present for ' + collectionSlug) + } + + let response: null | Response = null + let error: unknown + + for (const handler of uploadConfig.handlers) { + try { + const result = await handler(req, { + doc: null, + params: { + clientUploadContext, // Pass additional specific to adapters context returned from UploadHandler, then staticHandler can use them. + collection: collectionSlug, + filename, + }, + }) + if (result) { + response = result + } + // If we couldn't get the file from that handler, save the error and try other. + } catch (err) { + error = err + } + } + + if (!response) { + if (error) { + payload.logger.error(error) + } + + throw new APIError('Expected response from the upload handler.') + } + + req.file = { + name: filename, + data: Buffer.from(await response.arrayBuffer()), + mimetype: response.headers.get('Content-Type') || mimeType, + size, + } + } } } } diff --git a/packages/plugin-cloud-storage/.swcrc b/packages/plugin-cloud-storage/.swcrc index 14463f4b0..b4fb882ca 100644 --- a/packages/plugin-cloud-storage/.swcrc +++ b/packages/plugin-cloud-storage/.swcrc @@ -7,6 +7,15 @@ "syntax": "typescript", "tsx": true, "dts": true + }, + "transform": { + "react": { + "runtime": "automatic", + "pragmaFrag": "React.Fragment", + "throwIfNamespace": true, + "development": false, + "useBuiltins": true + } } }, "module": { diff --git a/packages/plugin-cloud-storage/package.json b/packages/plugin-cloud-storage/package.json index e78ed9d19..95f545628 100644 --- a/packages/plugin-cloud-storage/package.json +++ b/packages/plugin-cloud-storage/package.json @@ -33,6 +33,11 @@ "import": "./src/exports/utilities.ts", "types": "./src/exports/utilities.ts", "default": "./src/exports/utilities.ts" + }, + "./client": { + "import": "./src/exports/client.ts", + "types": "./src/exports/client.ts", + "default": "./src/exports/client.ts" } }, "main": "./src/index.ts", @@ -53,15 +58,20 @@ "test": "echo \"No tests available.\"" }, "dependencies": { + "@payloadcms/ui": "workspace:*", "find-node-modules": "^2.1.3", "range-parser": "^1.2.1" }, "devDependencies": { "@types/find-node-modules": "^2.1.2", + "@types/react": "19.0.1", + "@types/react-dom": "19.0.1", "payload": "workspace:*" }, "peerDependencies": { - "payload": "workspace:*" + "payload": "workspace:*", + "react": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020", + "react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020" }, "publishConfig": { "exports": { @@ -79,6 +89,11 @@ "import": "./dist/exports/utilities.js", "types": "./dist/exports/utilities.d.ts", "default": "./dist/exports/utilities.js" + }, + "./client": { + "import": "./dist/exports/client.js", + "types": "./dist/exports/client.d.ts", + "default": "./dist/exports/client.js" } }, "main": "./dist/index.js", diff --git a/packages/plugin-cloud-storage/src/client/createClientUploadHandler.tsx b/packages/plugin-cloud-storage/src/client/createClientUploadHandler.tsx new file mode 100644 index 000000000..a53c16d55 --- /dev/null +++ b/packages/plugin-cloud-storage/src/client/createClientUploadHandler.tsx @@ -0,0 +1,69 @@ +'use client' + +import type { UploadCollectionSlug } from 'payload' + +import { useConfig, useEffectEvent, useUploadHandlers } from '@payloadcms/ui' +import { Fragment, type ReactNode, useEffect } from 'react' + +type ClientUploadHandlerProps> = { + children: ReactNode + collectionSlug: UploadCollectionSlug + enabled?: boolean + extra: T + serverHandlerPath: string +} + +export const createClientUploadHandler = >({ + handler, +}: { + handler: (args: { + apiRoute: string + collectionSlug: UploadCollectionSlug + extra: T + file: File + serverHandlerPath: string + serverURL: string + updateFilename: (value: string) => void + }) => Promise +}) => { + return function ClientUploadHandler({ + children, + collectionSlug, + enabled, + extra, + serverHandlerPath, + }: ClientUploadHandlerProps) { + const { setUploadHandler } = useUploadHandlers() + const { + config: { + routes: { api: apiRoute }, + serverURL, + }, + } = useConfig() + + const initializeHandler = useEffectEvent(() => { + if (enabled) { + setUploadHandler({ + collectionSlug, + handler: ({ file, updateFilename }) => { + return handler({ + apiRoute, + collectionSlug, + extra, + file, + serverHandlerPath, + serverURL, + updateFilename, + }) + }, + }) + } + }) + + useEffect(() => { + initializeHandler() + }, []) + + return {children} + } +} diff --git a/packages/plugin-cloud-storage/src/exports/client.ts b/packages/plugin-cloud-storage/src/exports/client.ts new file mode 100644 index 000000000..e63875627 --- /dev/null +++ b/packages/plugin-cloud-storage/src/exports/client.ts @@ -0,0 +1 @@ +export { createClientUploadHandler } from '../client/createClientUploadHandler.js' diff --git a/packages/plugin-cloud-storage/src/exports/utilities.ts b/packages/plugin-cloud-storage/src/exports/utilities.ts index e36f9b33e..129de1ec8 100644 --- a/packages/plugin-cloud-storage/src/exports/utilities.ts +++ b/packages/plugin-cloud-storage/src/exports/utilities.ts @@ -1 +1,2 @@ export { getFilePrefix } from '../utilities/getFilePrefix.js' +export { initClientUploads } from '../utilities/initClientUploads.js' diff --git a/packages/plugin-cloud-storage/src/types.ts b/packages/plugin-cloud-storage/src/types.ts index 38ea00a98..ca27701e1 100644 --- a/packages/plugin-cloud-storage/src/types.ts +++ b/packages/plugin-cloud-storage/src/types.ts @@ -16,6 +16,17 @@ export interface File { tempFilePath?: string } +export type ClientUploadsAccess = (args: { + collectionSlug: UploadCollectionSlug + req: PayloadRequest +}) => boolean | Promise + +export type ClientUploadsConfig = + | { + access?: ClientUploadsAccess + } + | boolean + export type HandleUpload = (args: { collection: CollectionConfig data: any @@ -43,7 +54,10 @@ export type GenerateURL = (args: { export type StaticHandler = ( req: PayloadRequest, - args: { doc?: TypeWithID; params: { collection: string; filename: string } }, + args: { + doc?: TypeWithID + params: { clientUploadContext?: unknown; collection: string; filename: string } + }, ) => Promise | Response export interface GeneratedAdapter { diff --git a/packages/plugin-cloud-storage/src/utilities/initClientUploads.ts b/packages/plugin-cloud-storage/src/utilities/initClientUploads.ts new file mode 100644 index 000000000..316995c38 --- /dev/null +++ b/packages/plugin-cloud-storage/src/utilities/initClientUploads.ts @@ -0,0 +1,76 @@ +import type { Config, PayloadHandler } from 'payload' + +export const initClientUploads = , T>({ + clientHandler, + collections, + config, + enabled, + extraClientHandlerProps, + serverHandler, + serverHandlerPath, +}: { + /** Path to clientHandler component */ + clientHandler: string + collections: Record + config: Config + enabled: boolean + /** extra props to pass to the client handler */ + extraClientHandlerProps?: (collection: T) => ExtraProps + serverHandler: PayloadHandler + serverHandlerPath: string +}) => { + if (enabled) { + if (!config.endpoints) { + config.endpoints = [] + } + + /** + * Tracks how many times the same handler was already applied. + * This allows to apply the same plugin multiple times, for example + * to use different buckets for different collections. + */ + let handlerCount = 0 + + for (const endpoint of config.endpoints) { + if (endpoint.path === serverHandlerPath) { + handlerCount++ + } + } + + if (handlerCount) { + serverHandlerPath = `${serverHandlerPath}-${handlerCount}` + } + + config.endpoints.push({ + handler: serverHandler, + method: 'post', + path: serverHandlerPath, + }) + } + + if (!config.admin) { + config.admin = {} + } + + if (!config.admin.components) { + config.admin.components = {} + } + + if (!config.admin.components.providers) { + config.admin.components.providers = [] + } + + for (const collectionSlug in collections) { + const collection = collections[collectionSlug] + + config.admin.components.providers.push({ + clientProps: { + collectionSlug, + enabled, + extra: extraClientHandlerProps ? extraClientHandlerProps(collection) : undefined, + serverHandlerPath, + }, + path: clientHandler, + }) + } +} diff --git a/packages/plugin-cloud-storage/tsconfig.json b/packages/plugin-cloud-storage/tsconfig.json index 14564e071..6c57b9f88 100644 --- a/packages/plugin-cloud-storage/tsconfig.json +++ b/packages/plugin-cloud-storage/tsconfig.json @@ -5,5 +5,5 @@ "strict": false, "noUncheckedIndexedAccess": false, }, - "references": [{ "path": "../payload" }] + "references": [{ "path": "../payload" }, { "path": "../ui" }] } diff --git a/packages/storage-azure/README.md b/packages/storage-azure/README.md index 0a3e31911..d258e0165 100644 --- a/packages/storage-azure/README.md +++ b/packages/storage-azure/README.md @@ -14,6 +14,7 @@ pnpm add @payloadcms/storage-azure - Configure the `collections` object to specify which collections should use the Azure Blob Storage adapter. The slug _must_ match one of your existing collection slugs. - When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection. +- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method to your website. ```ts import { azureStorage } from '@payloadcms/storage-azure' @@ -49,3 +50,4 @@ export default buildConfig({ | `baseURL` | Base URL for the Azure Blob storage account | | | `connectionString` | Azure Blob storage connection string | | | `containerName` | Azure Blob storage container name | | +| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | | diff --git a/packages/storage-azure/package.json b/packages/storage-azure/package.json index 20d879384..62bba29ca 100644 --- a/packages/storage-azure/package.json +++ b/packages/storage-azure/package.json @@ -23,6 +23,11 @@ "import": "./src/index.ts", "types": "./src/index.ts", "default": "./src/index.ts" + }, + "./client": { + "import": "./src/exports/client.ts", + "types": "./src/exports/client.ts", + "default": "./src/exports/client.ts" } }, "main": "./src/index.ts", @@ -62,6 +67,11 @@ "import": "./dist/index.js", "types": "./dist/index.d.ts", "default": "./dist/index.js" + }, + "./client": { + "import": "./dist/exports/client.js", + "types": "./dist/exports/client.d.ts", + "default": "./dist/exports/client.js" } }, "main": "./dist/index.js", diff --git a/packages/storage-azure/src/client/AzureClientUploadHandler.ts b/packages/storage-azure/src/client/AzureClientUploadHandler.ts new file mode 100644 index 000000000..c61251fea --- /dev/null +++ b/packages/storage-azure/src/client/AzureClientUploadHandler.ts @@ -0,0 +1,29 @@ +'use client' +import { createClientUploadHandler } from '@payloadcms/plugin-cloud-storage/client' + +export const AzureClientUploadHandler = createClientUploadHandler({ + handler: async ({ apiRoute, collectionSlug, file, serverHandlerPath, serverURL }) => { + const response = await fetch(`${serverURL}${apiRoute}${serverHandlerPath}`, { + body: JSON.stringify({ + collectionSlug, + filename: file.name, + mimeType: file.type, + }), + credentials: 'include', + method: 'POST', + }) + + const { url } = await response.json() + + await fetch(url, { + body: file, + headers: { + 'Content-Length': file.size.toString(), + 'Content-Type': file.type, + // Required for azure + 'x-ms-blob-type': 'BlockBlob', + }, + method: 'PUT', + }) + }, +}) diff --git a/packages/storage-azure/src/exports/client.ts b/packages/storage-azure/src/exports/client.ts new file mode 100644 index 000000000..558c3e3f4 --- /dev/null +++ b/packages/storage-azure/src/exports/client.ts @@ -0,0 +1 @@ +export { AzureClientUploadHandler } from '../client/AzureClientUploadHandler.js' diff --git a/packages/storage-azure/src/generateSignedURL.ts b/packages/storage-azure/src/generateSignedURL.ts new file mode 100644 index 000000000..82219de4c --- /dev/null +++ b/packages/storage-azure/src/generateSignedURL.ts @@ -0,0 +1,62 @@ +import type { ContainerClient, StorageSharedKeyCredential } from '@azure/storage-blob' +import type { ClientUploadsAccess } from '@payloadcms/plugin-cloud-storage/types' +import type { PayloadHandler } from 'payload' + +import { BlobSASPermissions, generateBlobSASQueryParameters } from '@azure/storage-blob' +import path from 'path' +import { APIError, Forbidden } from 'payload' + +import type { AzureStorageOptions } from './index.js' + +interface Args { + access?: ClientUploadsAccess + collections: AzureStorageOptions['collections'] + containerName: string + getStorageClient: () => ContainerClient +} + +const defaultAccess: Args['access'] = ({ req }) => !!req.user + +export const getGenerateSignedURLHandler = ({ + access = defaultAccess, + collections, + containerName, + getStorageClient, +}: Args): PayloadHandler => { + return async (req) => { + if (!req.json) { + throw new APIError('Unreachable') + } + + const { collectionSlug, filename, mimeType } = await req.json() + + const collectionS3Config = collections[collectionSlug] + if (!collectionS3Config) { + throw new APIError(`Collection ${collectionSlug} was not found in S3 options`) + } + + const prefix = (typeof collectionS3Config === 'object' && collectionS3Config.prefix) || '' + + if (!(await access({ collectionSlug, req }))) { + throw new Forbidden() + } + + const fileKey = path.posix.join(prefix, filename) + + const blobClient = getStorageClient().getBlobClient(fileKey) + + const sasToken = generateBlobSASQueryParameters( + { + blobName: fileKey, + containerName, + contentType: mimeType, + expiresOn: new Date(Date.now() + 30 * 60 * 1000), + permissions: BlobSASPermissions.parse('w'), + startsOn: new Date(), + }, + getStorageClient().credential as StorageSharedKeyCredential, + ) + + return Response.json({ url: `${blobClient.url}?${sasToken.toString()}` }) + } +} diff --git a/packages/storage-azure/src/index.ts b/packages/storage-azure/src/index.ts index 4f9994b34..babc0810e 100644 --- a/packages/storage-azure/src/index.ts +++ b/packages/storage-azure/src/index.ts @@ -1,5 +1,7 @@ +import type { ContainerClient } from '@azure/storage-blob' import type { Adapter, + ClientUploadsConfig, PluginOptions as CloudStoragePluginOptions, CollectionOptions, GeneratedAdapter, @@ -7,7 +9,9 @@ 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 { getGenerateSignedURLHandler } from './generateSignedURL.js' import { getGenerateURL } from './generateURL.js' import { getHandleDelete } from './handleDelete.js' import { getHandleUpload } from './handleUpload.js' @@ -27,6 +31,11 @@ export type AzureStorageOptions = { */ baseURL: string + /** + * Do uploads directly on the client to bypass limits on Vercel. You must allow CORS PUT method to your website. + */ + clientUploads?: ClientUploadsConfig + /** * Collection options to apply the Azure Blob adapter to. */ @@ -59,7 +68,30 @@ export const azureStorage: AzureStoragePlugin = return incomingConfig } - const adapter = azureStorageInternal(azureStorageOptions) + const getStorageClient = () => + getStorageClientFunc({ + connectionString: azureStorageOptions.connectionString, + containerName: azureStorageOptions.containerName, + }) + + initClientUploads({ + clientHandler: '@payloadcms/storage-azure/client#AzureClientUploadHandler', + collections: azureStorageOptions.collections, + config: incomingConfig, + enabled: !!azureStorageOptions.clientUploads, + serverHandler: getGenerateSignedURLHandler({ + access: + typeof azureStorageOptions.clientUploads === 'object' + ? azureStorageOptions.clientUploads.access + : undefined, + collections: azureStorageOptions.collections, + containerName: azureStorageOptions.containerName, + getStorageClient, + }), + serverHandlerPath: '/storage-azure-generate-signed-url', + }) + + const adapter = azureStorageInternal(getStorageClient, azureStorageOptions) // Add adapter to each collection option object const collectionsWithAdapter: CloudStoragePluginOptions['collections'] = Object.entries( @@ -98,20 +130,16 @@ export const azureStorage: AzureStoragePlugin = })(config) } -function azureStorageInternal({ - allowContainerCreate, - baseURL, - connectionString, - containerName, -}: AzureStorageOptions): Adapter { +function azureStorageInternal( + getStorageClient: () => ContainerClient, + { allowContainerCreate, baseURL, connectionString, containerName }: AzureStorageOptions, +): Adapter { const createContainerIfNotExists = () => { void getStorageClientFunc({ connectionString, containerName }).createIfNotExists({ access: 'blob', }) } - const getStorageClient = () => getStorageClientFunc({ connectionString, containerName }) - return ({ collection, prefix }): GeneratedAdapter => { return { name: 'azure', diff --git a/packages/storage-gcs/README.md b/packages/storage-gcs/README.md index fc82b2fb2..7af85426a 100644 --- a/packages/storage-gcs/README.md +++ b/packages/storage-gcs/README.md @@ -14,6 +14,7 @@ pnpm add @payloadcms/storage-gcs - Configure the `collections` object to specify which collections should use the Google Cloud Storage adapter. The slug _must_ match one of your existing collection slugs. - When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection. +- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method for the bucket to your website. ```ts import { gcsStorage } from '@payloadcms/storage-gcs' diff --git a/packages/storage-gcs/package.json b/packages/storage-gcs/package.json index 4a0eb02ff..6aa313ee2 100644 --- a/packages/storage-gcs/package.json +++ b/packages/storage-gcs/package.json @@ -23,6 +23,11 @@ "import": "./src/index.ts", "types": "./src/index.ts", "default": "./src/index.ts" + }, + "./client": { + "import": "./src/exports/client.ts", + "types": "./src/exports/client.ts", + "default": "./src/exports/client.ts" } }, "main": "./src/index.ts", @@ -59,6 +64,11 @@ "import": "./dist/index.js", "types": "./dist/index.d.ts", "default": "./dist/index.js" + }, + "./client": { + "import": "./dist/exports/client.js", + "types": "./dist/exports/client.d.ts", + "default": "./dist/exports/client.js" } }, "main": "./dist/index.js", diff --git a/packages/storage-gcs/src/client/GcsClientUploadHandler.ts b/packages/storage-gcs/src/client/GcsClientUploadHandler.ts new file mode 100644 index 000000000..549c5a3c1 --- /dev/null +++ b/packages/storage-gcs/src/client/GcsClientUploadHandler.ts @@ -0,0 +1,24 @@ +'use client' +import { createClientUploadHandler } from '@payloadcms/plugin-cloud-storage/client' + +export const GcsClientUploadHandler = createClientUploadHandler({ + handler: async ({ apiRoute, collectionSlug, file, serverHandlerPath, serverURL }) => { + const response = await fetch(`${serverURL}${apiRoute}${serverHandlerPath}`, { + body: JSON.stringify({ + collectionSlug, + filename: file.name, + mimeType: file.type, + }), + credentials: 'include', + method: 'POST', + }) + + const { url } = await response.json() + + await fetch(url, { + body: file, + headers: { 'Content-Length': file.size.toString(), 'Content-Type': file.type }, + method: 'PUT', + }) + }, +}) diff --git a/packages/storage-gcs/src/exports/client.ts b/packages/storage-gcs/src/exports/client.ts new file mode 100644 index 000000000..ec55eda7e --- /dev/null +++ b/packages/storage-gcs/src/exports/client.ts @@ -0,0 +1 @@ +export { GcsClientUploadHandler } from '../client/GcsClientUploadHandler.js' diff --git a/packages/storage-gcs/src/generateSignedURL.ts b/packages/storage-gcs/src/generateSignedURL.ts new file mode 100644 index 000000000..acaf4a7f0 --- /dev/null +++ b/packages/storage-gcs/src/generateSignedURL.ts @@ -0,0 +1,58 @@ +import type { Storage } from '@google-cloud/storage' +import type { ClientUploadsAccess } from '@payloadcms/plugin-cloud-storage/types' +import type { PayloadHandler } from 'payload' + +import path from 'path' +import { APIError, Forbidden } from 'payload' + +import type { GcsStorageOptions } from './index.js' + +interface Args { + access?: ClientUploadsAccess + acl?: 'private' | 'public-read' + bucket: string + collections: GcsStorageOptions['collections'] + getStorageClient: () => Storage +} + +const defaultAccess: Args['access'] = ({ req }) => !!req.user + +export const getGenerateSignedURLHandler = ({ + access = defaultAccess, + bucket, + collections, + getStorageClient, +}: Args): PayloadHandler => { + return async (req) => { + if (!req.json) { + throw new APIError('Unreachable') + } + + const { collectionSlug, filename, mimeType } = await req.json() + + const collectionS3Config = collections[collectionSlug] + if (!collectionS3Config) { + throw new APIError(`Collection ${collectionSlug} was not found in S3 options`) + } + + const prefix = (typeof collectionS3Config === 'object' && collectionS3Config.prefix) || '' + + if (!(await access({ collectionSlug, req }))) { + throw new Forbidden() + } + + const fileKey = path.posix.join(prefix, filename) + + const [url] = await getStorageClient() + .bucket(bucket) + .file(fileKey) + .getSignedUrl({ + action: 'write', + contentType: mimeType, + expires: Date.now() + 60 * 60 * 5, + version: 'v4', + }) + + return Response.json({ url }) + } +} diff --git a/packages/storage-gcs/src/index.ts b/packages/storage-gcs/src/index.ts index ebd101766..2b06b2a37 100644 --- a/packages/storage-gcs/src/index.ts +++ b/packages/storage-gcs/src/index.ts @@ -1,6 +1,7 @@ import type { StorageOptions } from '@google-cloud/storage' import type { Adapter, + ClientUploadsConfig, PluginOptions as CloudStoragePluginOptions, CollectionOptions, GeneratedAdapter, @@ -10,6 +11,7 @@ import type { Config, Plugin, UploadCollectionSlug } from 'payload' import { Storage } from '@google-cloud/storage' import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage' +import { getGenerateSignedURLHandler } from './generateSignedURL.js' import { getGenerateURL } from './generateURL.js' import { getHandleDelete } from './handleDelete.js' import { getHandleUpload } from './handleUpload.js' @@ -22,6 +24,10 @@ export interface GcsStorageOptions { * The name of the bucket to use. */ bucket: string + /** + * Do uploads directly on the client to bypass limits on Vercel. You must allow CORS PUT method for the bucket to your website. + */ + clientUploads?: ClientUploadsConfig /** * Collection options to apply the S3 adapter to. */ @@ -50,7 +56,60 @@ export const gcsStorage: GcsStoragePlugin = return incomingConfig } - const adapter = gcsStorageInternal(gcsStorageOptions) + let storageClient: null | Storage = null + + const getStorageClient = (): Storage => { + if (storageClient) { + return storageClient + } + storageClient = new Storage(gcsStorageOptions.options) + + return storageClient + } + + const adapter = gcsStorageInternal(getStorageClient, gcsStorageOptions) + + if (gcsStorageOptions.clientUploads) { + if (!incomingConfig.endpoints) { + incomingConfig.endpoints = [] + } + + incomingConfig.endpoints.push({ + handler: getGenerateSignedURLHandler({ + access: + typeof gcsStorageOptions.clientUploads === 'object' + ? gcsStorageOptions.clientUploads.access + : undefined, + bucket: gcsStorageOptions.bucket, + collections: gcsStorageOptions.collections, + getStorageClient, + }), + method: 'post', + path: '/storage-gcs-generate-signed-url', + }) + } + + if (!incomingConfig.admin) { + incomingConfig.admin = {} + } + + if (!incomingConfig.admin.components) { + incomingConfig.admin.components = {} + } + + if (!incomingConfig.admin.components.providers) { + incomingConfig.admin.components.providers = [] + } + + for (const collectionSlug in gcsStorageOptions.collections) { + incomingConfig.admin.components.providers.push({ + clientProps: { + collectionSlug, + enabled: !!gcsStorageOptions.clientUploads, + }, + path: '@payloadcms/storage-gcs/client#GcsClientUploadHandler', + }) + } // Add adapter to each collection option object const collectionsWithAdapter: CloudStoragePluginOptions['collections'] = Object.entries( @@ -89,18 +148,11 @@ export const gcsStorage: GcsStoragePlugin = })(config) } -function gcsStorageInternal({ acl, bucket, options }: GcsStorageOptions): Adapter { +function gcsStorageInternal( + getStorageClient: () => Storage, + { acl, bucket }: GcsStorageOptions, +): Adapter { return ({ collection, prefix }): GeneratedAdapter => { - let storageClient: null | Storage = null - - const getStorageClient = (): Storage => { - if (storageClient) { - return storageClient - } - storageClient = new Storage(options) - return storageClient - } - return { name: 'gcs', generateURL: getGenerateURL({ bucket, getStorageClient }), diff --git a/packages/storage-s3/README.md b/packages/storage-s3/README.md index b187b56fe..ada7b756a 100644 --- a/packages/storage-s3/README.md +++ b/packages/storage-s3/README.md @@ -15,6 +15,7 @@ pnpm add @payloadcms/storage-s3 - Configure the `collections` object to specify which collections should use the AWS S3 adapter. The slug _must_ match one of your existing collection slugs. - The `config` object can be any [`S3ClientConfig`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3) object (from [`@aws-sdk/client-s3`](https://github.com/aws/aws-sdk-js-v3)). _This is highly dependent on your AWS setup_. Check the AWS documentation for more information. - When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection. +- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method for the bucket to your website. ```ts import { s3Storage } from '@payloadcms/storage-s3' diff --git a/packages/storage-s3/package.json b/packages/storage-s3/package.json index 5cad099a8..b095a7b77 100644 --- a/packages/storage-s3/package.json +++ b/packages/storage-s3/package.json @@ -23,6 +23,11 @@ "import": "./src/index.ts", "types": "./src/index.ts", "default": "./src/index.ts" + }, + "./client": { + "import": "./src/exports/client.ts", + "types": "./src/exports/client.ts", + "default": "./src/exports/client.ts" } }, "main": "./src/index.ts", @@ -43,6 +48,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.614.0", "@aws-sdk/lib-storage": "^3.614.0", + "@aws-sdk/s3-request-presigner": "^3.614.0", "@payloadcms/plugin-cloud-storage": "workspace:*" }, "devDependencies": { @@ -60,6 +66,11 @@ "import": "./dist/index.js", "types": "./dist/index.d.ts", "default": "./dist/index.js" + }, + "./client": { + "import": "./dist/exports/client.js", + "types": "./dist/exports/client.d.ts", + "default": "./dist/exports/client.js" } }, "main": "./dist/index.js", diff --git a/packages/storage-s3/src/client/S3ClientUploadHandler.ts b/packages/storage-s3/src/client/S3ClientUploadHandler.ts new file mode 100644 index 000000000..7f1748a89 --- /dev/null +++ b/packages/storage-s3/src/client/S3ClientUploadHandler.ts @@ -0,0 +1,24 @@ +'use client' +import { createClientUploadHandler } from '@payloadcms/plugin-cloud-storage/client' + +export const S3ClientUploadHandler = createClientUploadHandler({ + handler: async ({ apiRoute, collectionSlug, file, serverHandlerPath, serverURL }) => { + const response = await fetch(`${serverURL}${apiRoute}${serverHandlerPath}`, { + body: JSON.stringify({ + collectionSlug, + filename: file.name, + mimeType: file.type, + }), + credentials: 'include', + method: 'POST', + }) + + const { url } = await response.json() + + await fetch(url, { + body: file, + headers: { 'Content-Length': file.size.toString(), 'Content-Type': file.type }, + method: 'PUT', + }) + }, +}) diff --git a/packages/storage-s3/src/exports/client.ts b/packages/storage-s3/src/exports/client.ts new file mode 100644 index 000000000..2da0d9d5d --- /dev/null +++ b/packages/storage-s3/src/exports/client.ts @@ -0,0 +1 @@ +export { S3ClientUploadHandler } from '../client/S3ClientUploadHandler.js' diff --git a/packages/storage-s3/src/generateSignedURL.ts b/packages/storage-s3/src/generateSignedURL.ts new file mode 100644 index 000000000..a194f919f --- /dev/null +++ b/packages/storage-s3/src/generateSignedURL.ts @@ -0,0 +1,59 @@ +import type { ClientUploadsAccess } from '@payloadcms/plugin-cloud-storage/types' +import type { PayloadHandler } from 'payload' + +import * as AWS from '@aws-sdk/client-s3' +import { getSignedUrl } from '@aws-sdk/s3-request-presigner' +import path from 'path' +import { APIError, Forbidden } from 'payload' + +import type { S3StorageOptions } from './index.js' + +interface Args { + access?: ClientUploadsAccess + acl?: 'private' | 'public-read' + bucket: string + collections: S3StorageOptions['collections'] + getStorageClient: () => AWS.S3 +} + +const defaultAccess: Args['access'] = ({ req }) => !!req.user + +export const getGenerateSignedURLHandler = ({ + access = defaultAccess, + acl, + bucket, + collections, + getStorageClient, +}: Args): PayloadHandler => { + return async (req) => { + if (!req.json) { + throw new APIError('Content-Type expected to be application/json', 400) + } + + const { collectionSlug, filename, mimeType } = await req.json() + + const collectionS3Config = collections[collectionSlug] + if (!collectionS3Config) { + throw new APIError(`Collection ${collectionSlug} was not found in S3 options`) + } + + const prefix = (typeof collectionS3Config === 'object' && collectionS3Config.prefix) || '' + + if (!(await access({ collectionSlug, req }))) { + throw new Forbidden() + } + + const fileKey = path.posix.join(prefix, filename) + + const url = await getSignedUrl( + // @ts-expect-error mismatch versions or something + getStorageClient(), + new AWS.PutObjectCommand({ ACL: acl, Bucket: bucket, ContentType: mimeType, Key: fileKey }), + { + expiresIn: 600, + }, + ) + + return Response.json({ url }) + } +} diff --git a/packages/storage-s3/src/index.ts b/packages/storage-s3/src/index.ts index d2a2efbe7..2fba35cc2 100644 --- a/packages/storage-s3/src/index.ts +++ b/packages/storage-s3/src/index.ts @@ -1,5 +1,6 @@ import type { Adapter, + ClientUploadsConfig, PluginOptions as CloudStoragePluginOptions, CollectionOptions, GeneratedAdapter, @@ -8,7 +9,9 @@ import type { Config, Plugin, UploadCollectionSlug } from 'payload' import * as AWS from '@aws-sdk/client-s3' import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage' +import { initClientUploads } from '@payloadcms/plugin-cloud-storage/utilities' +import { getGenerateSignedURLHandler } from './generateSignedURL.js' import { getGenerateURL } from './generateURL.js' import { getHandleDelete } from './handleDelete.js' import { getHandleUpload } from './handleUpload.js' @@ -28,10 +31,15 @@ export type S3StorageOptions = { bucket: string + /** + * Do uploads directly on the client to bypass limits on Vercel. You must allow CORS PUT method for the bucket to your website. + */ + clientUploads?: ClientUploadsConfig /** * Collection options to apply the S3 adapter to. */ collections: Partial | true>> + /** * AWS S3 client configuration. Highly dependent on your AWS setup. * @@ -63,7 +71,35 @@ export const s3Storage: S3StoragePlugin = return incomingConfig } - const adapter = s3StorageInternal(s3StorageOptions) + let storageClient: AWS.S3 | null = null + + const getStorageClient: () => AWS.S3 = () => { + if (storageClient) { + return storageClient + } + storageClient = new AWS.S3(s3StorageOptions.config ?? {}) + return storageClient + } + + initClientUploads({ + clientHandler: '@payloadcms/storage-s3/client#S3ClientUploadHandler', + collections: s3StorageOptions.collections, + config: incomingConfig, + enabled: !!s3StorageOptions.clientUploads, + serverHandler: getGenerateSignedURLHandler({ + access: + typeof s3StorageOptions.clientUploads === 'object' + ? s3StorageOptions.clientUploads.access + : undefined, + acl: s3StorageOptions.acl, + bucket: s3StorageOptions.bucket, + collections: s3StorageOptions.collections, + getStorageClient, + }), + serverHandlerPath: '/storage-s3-generate-signed-url', + }) + + const adapter = s3StorageInternal(getStorageClient, s3StorageOptions) // Add adapter to each collection option object const collectionsWithAdapter: CloudStoragePluginOptions['collections'] = Object.entries( @@ -102,17 +138,11 @@ export const s3Storage: S3StoragePlugin = })(config) } -function s3StorageInternal({ acl, bucket, config = {} }: S3StorageOptions): Adapter { +function s3StorageInternal( + getStorageClient: () => AWS.S3, + { acl, bucket, config = {} }: S3StorageOptions, +): Adapter { return ({ collection, prefix }): GeneratedAdapter => { - let storageClient: AWS.S3 | null = null - const getStorageClient: () => AWS.S3 = () => { - if (storageClient) { - return storageClient - } - storageClient = new AWS.S3(config) - return storageClient - } - return { name: 's3', generateURL: getGenerateURL({ bucket, config }), diff --git a/packages/storage-uploadthing/README.md b/packages/storage-uploadthing/README.md index 730b64e1c..595fa3597 100644 --- a/packages/storage-uploadthing/README.md +++ b/packages/storage-uploadthing/README.md @@ -13,6 +13,7 @@ pnpm add @payloadcms/storage-uploadthing - Configure the `collections` object to specify which collections should use uploadthing. The slug _must_ match one of your existing collection slugs and be an `upload` type. - Get an API key from Uploadthing and set it as `apiKey` in the `options` object. - `acl` is optional and defaults to `public-read`. +- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. ```ts export default buildConfig({ diff --git a/packages/storage-uploadthing/package.json b/packages/storage-uploadthing/package.json index b099b728a..d03707d1d 100644 --- a/packages/storage-uploadthing/package.json +++ b/packages/storage-uploadthing/package.json @@ -23,6 +23,11 @@ "import": "./src/index.ts", "types": "./src/index.ts", "default": "./src/index.ts" + }, + "./client": { + "import": "./src/exports/client.ts", + "types": "./src/exports/client.ts", + "default": "./src/exports/client.ts" } }, "main": "./src/index.ts", @@ -59,6 +64,11 @@ "import": "./dist/index.js", "types": "./dist/index.d.ts", "default": "./dist/index.js" + }, + "./client": { + "import": "./dist/exports/client.js", + "types": "./dist/exports/client.d.ts", + "default": "./dist/exports/client.js" } }, "main": "./dist/index.js", diff --git a/packages/storage-uploadthing/src/client/UploadthingClientUploadHandler.ts b/packages/storage-uploadthing/src/client/UploadthingClientUploadHandler.ts new file mode 100644 index 000000000..e2d248e77 --- /dev/null +++ b/packages/storage-uploadthing/src/client/UploadthingClientUploadHandler.ts @@ -0,0 +1,18 @@ +'use client' +import { createClientUploadHandler } from '@payloadcms/plugin-cloud-storage/client' +import { genUploader } from 'uploadthing/client' + +export const UploadthingClientUploadHandler = createClientUploadHandler({ + handler: async ({ apiRoute, collectionSlug, file, serverHandlerPath, serverURL }) => { + const { uploadFiles } = genUploader({ + package: 'storage-uploadthing', + url: `${serverURL}${apiRoute}${serverHandlerPath}?collectionSlug=${collectionSlug}`, + }) + + const res = await uploadFiles('uploader', { + files: [file], + }) + + return { key: res[0].key } + }, +}) diff --git a/packages/storage-uploadthing/src/exports/client.ts b/packages/storage-uploadthing/src/exports/client.ts new file mode 100644 index 000000000..e430c1ee3 --- /dev/null +++ b/packages/storage-uploadthing/src/exports/client.ts @@ -0,0 +1 @@ +export { UploadthingClientUploadHandler } from '../client/UploadthingClientUploadHandler.js' diff --git a/packages/storage-uploadthing/src/getClientUploadRoute.ts b/packages/storage-uploadthing/src/getClientUploadRoute.ts new file mode 100644 index 000000000..2eea6b146 --- /dev/null +++ b/packages/storage-uploadthing/src/getClientUploadRoute.ts @@ -0,0 +1,62 @@ +import { + APIError, + Forbidden, + type PayloadHandler, + type PayloadRequest, + type UploadCollectionSlug, +} from 'payload' + +type Args = { + access?: (args: { + collectionSlug: UploadCollectionSlug + req: PayloadRequest + }) => boolean | Promise + acl: 'private' | 'public-read' + token?: string +} + +const defaultAccess: Args['access'] = ({ req }) => !!req.user + +import type { FileRouter } from 'uploadthing/server' + +import { createRouteHandler } from 'uploadthing/next' +import { createUploadthing } from 'uploadthing/server' + +export const getClientUploadRoute = ({ + access = defaultAccess, + acl, + token, +}: Args): PayloadHandler => { + const f = createUploadthing() + + const uploadRouter = { + uploader: f({ + blob: { + acl, + maxFileCount: 1, + }, + }) + .middleware(async ({ req: rawReq }) => { + const req = rawReq as PayloadRequest + + const collectionSlug = req.searchParams.get('collectionSlug') + + if (!collectionSlug) { + throw new APIError('No payload was provided') + } + + if (!(await access({ collectionSlug, req }))) { + throw new Forbidden() + } + + return {} + }) + .onUploadComplete(() => {}), + } satisfies FileRouter + + const { POST } = createRouteHandler({ config: { token }, router: uploadRouter }) + + return async (req) => { + return POST(req) + } +} diff --git a/packages/storage-uploadthing/src/index.ts b/packages/storage-uploadthing/src/index.ts index 4c472c6f7..b7af504a3 100644 --- a/packages/storage-uploadthing/src/index.ts +++ b/packages/storage-uploadthing/src/index.ts @@ -1,5 +1,6 @@ import type { Adapter, + ClientUploadsConfig, PluginOptions as CloudStoragePluginOptions, CollectionOptions, GeneratedAdapter, @@ -8,14 +9,22 @@ import type { Config, Field, Plugin, UploadCollectionSlug } from 'payload' import type { UTApiOptions } from 'uploadthing/types' import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage' -import { UTApi } from 'uploadthing/server' +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. */ @@ -58,6 +67,22 @@ export const uploadthingStorage: UploadthingPlugin = 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, diff --git a/packages/storage-uploadthing/src/staticHandler.ts b/packages/storage-uploadthing/src/staticHandler.ts index cd25ebdbe..39aaff933 100644 --- a/packages/storage-uploadthing/src/staticHandler.ts +++ b/packages/storage-uploadthing/src/staticHandler.ts @@ -9,47 +9,58 @@ type Args = { } export const getHandler = ({ utApi }: Args): StaticHandler => { - return async (req, { doc, params: { collection, filename } }) => { + return async (req, { doc, params: { clientUploadContext, collection, filename } }) => { try { - const collectionConfig = req.payload.collections[collection]?.config - let retrievedDoc = doc + let key: string - if (!retrievedDoc) { - const or: Where[] = [ - { - filename: { - equals: filename, - }, - }, - ] + 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 (collectionConfig.upload.imageSizes) { - collectionConfig.upload.imageSizes.forEach(({ name }) => { - or.push({ - [`sizes.${name}.filename`]: { + 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 + } } - 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' }) } - } - if (!retrievedDoc) { - return new Response(null, { status: 404, statusText: 'Not Found' }) + key = getKeyFromFilename(retrievedDoc, filename) } - const key = getKeyFromFilename(retrievedDoc, filename) - if (!key) { return new Response(null, { status: 404, statusText: 'Not Found' }) } @@ -69,7 +80,7 @@ export const getHandler = ({ utApi }: Args): StaticHandler => { const blob = await response.blob() const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match') - const objectEtag = response.headers.get('etag') as string + const objectEtag = response.headers.get('etag') if (etagFromHeaders && etagFromHeaders === objectEtag) { return new Response(null, { diff --git a/packages/storage-vercel-blob/README.md b/packages/storage-vercel-blob/README.md index 09be8dfd9..1ec980965 100644 --- a/packages/storage-vercel-blob/README.md +++ b/packages/storage-vercel-blob/README.md @@ -15,6 +15,7 @@ pnpm add @payloadcms/storage-vercel-blob - Configure the `collections` object to specify which collections should use the Vercel Blob adapter. The slug _must_ match one of your existing collection slugs. - Ensure you have `BLOB_READ_WRITE_TOKEN` set in your Vercel environment variables. This is usually set by Vercel automatically after adding blob storage to your project. - When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection. +- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. ```ts import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob' @@ -47,3 +48,4 @@ export default buildConfig({ | `addRandomSuffix` | Add a random suffix to the uploaded file name in Vercel Blob storage | `false` | | `cacheControlMaxAge` | Cache-Control max-age in seconds | `365 * 24 * 60 * 60` (1 Year) | | `token` | Vercel Blob storage read/write token | `''` | +| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel | | diff --git a/packages/storage-vercel-blob/package.json b/packages/storage-vercel-blob/package.json index ed9fa90e7..fc64e6c5a 100644 --- a/packages/storage-vercel-blob/package.json +++ b/packages/storage-vercel-blob/package.json @@ -23,6 +23,11 @@ "import": "./src/index.ts", "types": "./src/index.ts", "default": "./src/index.ts" + }, + "./client": { + "import": "./src/exports/client.ts", + "types": "./src/exports/client.ts", + "default": "./src/exports/client.ts" } }, "main": "./src/index.ts", @@ -59,6 +64,11 @@ "import": "./dist/index.js", "types": "./dist/index.d.ts", "default": "./dist/index.js" + }, + "./client": { + "import": "./dist/exports/client.js", + "types": "./dist/exports/client.d.ts", + "default": "./dist/exports/client.js" } }, "main": "./dist/index.js", diff --git a/packages/storage-vercel-blob/src/client/VercelBlobClientUploadHandler.ts b/packages/storage-vercel-blob/src/client/VercelBlobClientUploadHandler.ts new file mode 100644 index 000000000..f283d2447 --- /dev/null +++ b/packages/storage-vercel-blob/src/client/VercelBlobClientUploadHandler.ts @@ -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({ + 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}/`, '')) + } + }, + }) diff --git a/packages/storage-vercel-blob/src/exports/client.ts b/packages/storage-vercel-blob/src/exports/client.ts new file mode 100644 index 000000000..f02956cc5 --- /dev/null +++ b/packages/storage-vercel-blob/src/exports/client.ts @@ -0,0 +1 @@ +export { VercelBlobClientUploadHandler } from '../client/VercelBlobClientUploadHandler.js' diff --git a/packages/storage-vercel-blob/src/getClientUploadRoute.ts b/packages/storage-vercel-blob/src/getClientUploadRoute.ts new file mode 100644 index 000000000..0dc2d6434 --- /dev/null +++ b/packages/storage-vercel-blob/src/getClientUploadRoute.ts @@ -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 + 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') + } + } diff --git a/packages/storage-vercel-blob/src/index.ts b/packages/storage-vercel-blob/src/index.ts index e0add0363..3097c6ebe 100644 --- a/packages/storage-vercel-blob/src/index.ts +++ b/packages/storage-vercel-blob/src/index.ts @@ -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 diff --git a/packages/storage-vercel-blob/src/staticHandler.ts b/packages/storage-vercel-blob/src/staticHandler.ts index 203ad27b2..008576f33 100644 --- a/packages/storage-vercel-blob/src/staticHandler.ts +++ b/packages/storage-vercel-blob/src/staticHandler.ts @@ -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}"` diff --git a/packages/ui/src/elements/BulkUpload/FormsManager/createFormData.ts b/packages/ui/src/elements/BulkUpload/FormsManager/createFormData.ts index 6adfedd70..68d059430 100644 --- a/packages/ui/src/elements/BulkUpload/FormsManager/createFormData.ts +++ b/packages/ui/src/elements/BulkUpload/FormsManager/createFormData.ts @@ -1,16 +1,43 @@ -import type { FormState } from 'payload' +import type { CollectionSlug, FormState } from 'payload' import { serialize } from 'object-to-formdata' import { reduceFieldsToValues } from 'payload/shared' -export function createFormData(formState: FormState = {}, overrides: Record = {}) { +import type { UploadHandlersContext } from '../../../providers/UploadHandlers/index.js' + +export async function createFormData( + formState: FormState = {}, + overrides: Record = {}, + collectionSlug: CollectionSlug, + uploadHandler: ReturnType, +) { const data = reduceFieldsToValues(formState, true) - const file = data?.file + let file = data?.file if (file) { delete data.file } + let clientUploadContext = null + + if (typeof uploadHandler === 'function') { + let filename = file.name + clientUploadContext = await uploadHandler({ + file, + updateFilename: (value) => { + filename = value + }, + }) + + file = JSON.stringify({ + clientUploadContext, + collectionSlug, + filename, + mimeType: file.type, + size: file.size, + }) + } + const dataWithOverrides = { ...data, ...overrides, diff --git a/packages/ui/src/elements/BulkUpload/FormsManager/index.tsx b/packages/ui/src/elements/BulkUpload/FormsManager/index.tsx index 965d90478..628d03544 100644 --- a/packages/ui/src/elements/BulkUpload/FormsManager/index.tsx +++ b/packages/ui/src/elements/BulkUpload/FormsManager/index.tsx @@ -15,6 +15,7 @@ import { useConfig } from '../../../providers/Config/index.js' import { useLocale } from '../../../providers/Locale/index.js' import { useServerFunctions } from '../../../providers/ServerFunctions/index.js' import { useTranslation } from '../../../providers/Translation/index.js' +import { useUploadHandlers } from '../../../providers/UploadHandlers/index.js' import { hasSavePermission as getHasSavePermission } from '../../../utilities/hasSavePermission.js' import { LoadingOverlay } from '../../Loading/index.js' import { useLoadingOverlay } from '../../LoadingOverlay/index.js' @@ -94,6 +95,7 @@ export function FormsManagerProvider({ children }: FormsManagerProps) { const { i18n, t } = useTranslation() const { getDocumentSlots, getFormState } = useServerFunctions() + const { getUploadHandler } = useUploadHandlers() const [documentSlots, setDocumentSlots] = React.useState({}) const [hasSubmitted, setHasSubmitted] = React.useState(false) @@ -296,7 +298,12 @@ export function FormsManagerProvider({ children }: FormsManagerProps) { setLoadingText(t('general:uploadingBulk', { current: i + 1, total: currentForms.length })) const req = await fetch(actionURL, { - body: createFormData(form.formState, overrides), + body: await createFormData( + form.formState, + overrides, + collectionSlug, + getUploadHandler({ collectionSlug }), + ), method: 'POST', }) @@ -387,7 +394,17 @@ export function FormsManagerProvider({ children }: FormsManagerProps) { }, }) }, - [actionURL, activeIndex, forms, onSuccess, t, closeModal, drawerSlug], + [ + actionURL, + activeIndex, + forms, + onSuccess, + collectionSlug, + getUploadHandler, + t, + closeModal, + drawerSlug, + ], ) const bulkUpdateForm = React.useCallback( diff --git a/packages/ui/src/exports/client/index.ts b/packages/ui/src/exports/client/index.ts index 2dccdfc45..870044f2e 100644 --- a/packages/ui/src/exports/client/index.ts +++ b/packages/ui/src/exports/client/index.ts @@ -290,6 +290,8 @@ export { export { ScrollInfoProvider, useScrollInfo } from '../../providers/ScrollInfo/index.js' export { SearchParamsProvider, useSearchParams } from '../../providers/SearchParams/index.js' export { SelectionProvider, useSelection } from '../../providers/Selection/index.js' +export { UploadHandlersProvider, useUploadHandlers } from '../../providers/UploadHandlers/index.js' +export type { UploadHandlersContext } from '../../providers/UploadHandlers/index.js' export { defaultTheme, type Theme, ThemeProvider, useTheme } from '../../providers/Theme/index.js' export { TranslationProvider, useTranslation } from '../../providers/Translation/index.js' export { useWindowInfo, WindowInfoProvider } from '../../providers/WindowInfo/index.js' diff --git a/packages/ui/src/forms/Form/index.tsx b/packages/ui/src/forms/Form/index.tsx index f2c035668..5fc0f2cd2 100644 --- a/packages/ui/src/forms/Form/index.tsx +++ b/packages/ui/src/forms/Form/index.tsx @@ -32,6 +32,7 @@ import { useOperation } from '../../providers/Operation/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useServerFunctions } from '../../providers/ServerFunctions/index.js' import { useTranslation } from '../../providers/Translation/index.js' +import { useUploadHandlers } from '../../providers/UploadHandlers/index.js' import { abortAndIgnore, handleAbortRef } from '../../utilities/abortAndIgnore.js' import { requests } from '../../utilities/api.js' import { @@ -90,6 +91,7 @@ export const Form: React.FC = (props) => { const { getFormState } = useServerFunctions() const { startRouteTransition } = useRouteTransition() + const { getUploadHandler } = useUploadHandlers() const { config } = useConfig() @@ -319,7 +321,7 @@ export const Form: React.FC = (props) => { return } - const formData = contextRef.current.createFormData(overrides, { + const formData = await contextRef.current.createFormData(overrides, { mergeOverrideData: Boolean(typeof overridesFromArgs !== 'function'), }) @@ -480,34 +482,58 @@ export const Form: React.FC = (props) => { [], ) - const createFormData = useCallback((overrides, { mergeOverrideData = true }) => { - let data = reduceFieldsToValues(contextRef.current.fields, true) + const createFormData = useCallback( + async (overrides, { mergeOverrideData = true }) => { + let data = reduceFieldsToValues(contextRef.current.fields, true) - const file = data?.file + let file = data?.file - if (file) { - delete data.file - } - - if (mergeOverrideData) { - data = { - ...data, - ...overrides, + if (file) { + delete data.file } - } else { - data = overrides - } - const dataToSerialize = { - _payload: JSON.stringify(data), - file, - } + if (mergeOverrideData) { + data = { + ...data, + ...overrides, + } + } else { + data = overrides + } - // nullAsUndefineds is important to allow uploads and relationship fields to clear themselves - const formData = serialize(dataToSerialize, { indices: true, nullsAsUndefineds: false }) + const handler = getUploadHandler({ collectionSlug }) - return formData - }, []) + if (typeof handler === 'function') { + let clientUploadContext = null + let filename = file.name + clientUploadContext = await handler({ + file, + updateFilename: (value) => { + filename = value + }, + }) + + file = JSON.stringify({ + clientUploadContext, + collectionSlug, + filename, + mimeType: file.type, + size: file.size, + }) + } + + const dataToSerialize = { + _payload: JSON.stringify(data), + file, + } + + // nullAsUndefineds is important to allow uploads and relationship fields to clear themselves + const formData = serialize(dataToSerialize, { indices: true, nullsAsUndefineds: false }) + + return formData + }, + [collectionSlug, getUploadHandler], + ) const reset = useCallback( async (data: unknown) => { diff --git a/packages/ui/src/forms/Form/types.ts b/packages/ui/src/forms/Form/types.ts index b8a33ee82..5455e189a 100644 --- a/packages/ui/src/forms/Form/types.ts +++ b/packages/ui/src/forms/Form/types.ts @@ -84,7 +84,7 @@ export type CreateFormData = ( * @default true */ options?: { mergeOverrideData?: boolean }, -) => FormData +) => FormData | Promise export type GetFields = () => FormState export type GetField = (path: string) => FormField export type GetData = () => Data diff --git a/packages/ui/src/providers/Root/index.tsx b/packages/ui/src/providers/Root/index.tsx index 429146827..7f94aa2c3 100644 --- a/packages/ui/src/providers/Root/index.tsx +++ b/packages/ui/src/providers/Root/index.tsx @@ -34,6 +34,7 @@ import { ServerFunctionsProvider } from '../ServerFunctions/index.js' import { ThemeProvider } from '../Theme/index.js' import { ToastContainer } from '../ToastContainer/index.js' import { TranslationProvider } from '../Translation/index.js' +import { UploadHandlersProvider } from '../UploadHandlers/index.js' type Props = { readonly children: React.ReactNode @@ -106,7 +107,9 @@ export const RootProvider: React.FC = ({ - {children} + + {children} + diff --git a/packages/ui/src/providers/UploadHandlers/index.tsx b/packages/ui/src/providers/UploadHandlers/index.tsx new file mode 100644 index 000000000..b3ac8009d --- /dev/null +++ b/packages/ui/src/providers/UploadHandlers/index.tsx @@ -0,0 +1,54 @@ +'use client' +import type { UploadCollectionSlug } from 'payload' + +import React, { useState } from 'react' + +type UploadHandler = (args: { + file: File + updateFilename: (filename: string) => void +}) => Promise + +export type UploadHandlersContext = { + getUploadHandler: (args: { collectionSlug: UploadCollectionSlug }) => null | UploadHandler + setUploadHandler: (args: { + collectionSlug: UploadCollectionSlug + handler: UploadHandler + }) => unknown +} + +const Context = React.createContext(null) + +export const UploadHandlersProvider = ({ children }) => { + const [uploadHandlers, setUploadHandlers] = useState>( + () => new Map(), + ) + + const getUploadHandler: UploadHandlersContext['getUploadHandler'] = ({ collectionSlug }) => { + return uploadHandlers.get(collectionSlug) + } + + const setUploadHandler: UploadHandlersContext['setUploadHandler'] = ({ + collectionSlug, + handler, + }) => { + setUploadHandlers((uploadHandlers) => { + const clone = new Map(uploadHandlers) + clone.set(collectionSlug, handler) + return clone + }) + } + + return ( + {children} + ) +} + +export const useUploadHandlers = (): UploadHandlersContext => { + const context = React.useContext(Context) + + if (context === null) { + throw new Error('useUploadHandlers must be used within UploadHandlersProvider') + } + + return context +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 033685d85..4bba2b98c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -956,16 +956,31 @@ importers: packages/plugin-cloud-storage: dependencies: + '@payloadcms/ui': + specifier: workspace:* + version: link:../ui find-node-modules: specifier: ^2.1.3 version: 2.1.3 range-parser: specifier: ^1.2.1 version: 1.2.1 + react: + specifier: 19.0.0 + version: 19.0.0 + react-dom: + specifier: 19.0.0 + version: 19.0.0(react@19.0.0) devDependencies: '@types/find-node-modules': specifier: ^2.1.2 version: 2.1.2 + '@types/react': + specifier: 19.0.1 + version: 19.0.1 + '@types/react-dom': + specifier: 19.0.1 + version: 19.0.1 payload: specifier: workspace:* version: link:../payload @@ -1408,6 +1423,9 @@ importers: '@aws-sdk/lib-storage': specifier: ^3.614.0 version: 3.687.0(@aws-sdk/client-s3@3.687.0) + '@aws-sdk/s3-request-presigner': + specifier: ^3.614.0 + version: 3.750.0 '@payloadcms/plugin-cloud-storage': specifier: workspace:* version: link:../plugin-cloud-storage @@ -1929,6 +1947,10 @@ packages: resolution: {integrity: sha512-Xt3DV4DnAT3v2WURwzTxWQK34Ew+iiLzoUoguvLaZrVMFOqMMrwVjP+sizqIaHp1j7rGmFcN5I8saXnsDLuQLA==} engines: {node: '>=16.0.0'} + '@aws-sdk/core@3.750.0': + resolution: {integrity: sha512-bZ5K7N5L4+Pa2epbVpUQqd1XLG2uU8BGs/Sd+2nbgTf+lNQJyIxAg/Qsrjz9MzmY8zzQIeRQEkNmR6yVAfCmmQ==} + engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-cognito-identity@3.687.0': resolution: {integrity: sha512-hJq9ytoj2q/Jonc7mox/b0HT+j4NeMRuU184DkXRJbvIvwwB+oMt12221kThLezMhwIYfXEteZ7GEId7Hn8Y8g==} engines: {node: '>=16.0.0'} @@ -2007,6 +2029,10 @@ packages: resolution: {integrity: sha512-YGHYqiyRiNNucmvLrfx3QxIkjSDWR/+cc72bn0lPvqFUQBRHZgmYQLxVYrVZSmRzzkH2FQ1HsZcXhOafLbq4vQ==} engines: {node: '>=16.0.0'} + '@aws-sdk/middleware-sdk-s3@3.750.0': + resolution: {integrity: sha512-3H6Z46cmAQCHQ0z8mm7/cftY5ifiLfCjbObrbyyp2fhQs9zk6gCKzIX8Zjhw0RMd93FZi3ebRuKJWmMglf4Itw==} + engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-ssec@3.686.0': resolution: {integrity: sha512-zJXml/CpVHFUdlGQqja87vNQ3rPB5SlDbfdwxlj1KBbjnRRwpBtxxmOlWRShg8lnVV6aIMGv95QmpIFy4ayqnQ==} engines: {node: '>=16.0.0'} @@ -2019,10 +2045,18 @@ packages: resolution: {integrity: sha512-6zXD3bSD8tcsMAVVwO1gO7rI1uy2fCD3czgawuPGPopeLiPpo6/3FoUWCQzk2nvEhj7p9Z4BbjwZGSlRkVrXTw==} engines: {node: '>=16.0.0'} + '@aws-sdk/s3-request-presigner@3.750.0': + resolution: {integrity: sha512-G4GNngNQlh9EyJZj2WKOOikX0Fev1WSxTV/XJugaHlpnVriebvi3GzolrgxUpRrcGpFGWjmAxLi/gYxTUla1ow==} + engines: {node: '>=18.0.0'} + '@aws-sdk/signature-v4-multi-region@3.687.0': resolution: {integrity: sha512-vdOQHCRHJPX9mT8BM6xOseazHD6NodvHl9cyF5UjNtLn+gERRJEItIA9hf0hlt62odGD8Fqp+rFRuqdmbNkcNw==} engines: {node: '>=16.0.0'} + '@aws-sdk/signature-v4-multi-region@3.750.0': + resolution: {integrity: sha512-RA9hv1Irro/CrdPcOEXKwJ0DJYJwYCsauGEdRXihrRfy8MNSR9E+mD5/Fr5Rxjaq5AHM05DYnN3mg/DU6VwzSw==} + engines: {node: '>=18.0.0'} + '@aws-sdk/token-providers@3.686.0': resolution: {integrity: sha512-9oL4kTCSePFmyKPskibeiOXV6qavPZ63/kXM9Wh9V6dTSvBtLeNnMxqGvENGKJcTdIgtoqyqA6ET9u0PJ5IRIg==} engines: {node: '>=16.0.0'} @@ -2033,14 +2067,26 @@ packages: resolution: {integrity: sha512-xFnrb3wxOoJcW2Xrh63ZgFo5buIu9DF7bOHnwoUxHdNpUXicUh0AHw85TjXxyxIAd0d1psY/DU7QHoNI3OswgQ==} engines: {node: '>=16.0.0'} + '@aws-sdk/types@3.734.0': + resolution: {integrity: sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg==} + engines: {node: '>=18.0.0'} + '@aws-sdk/util-arn-parser@3.679.0': resolution: {integrity: sha512-CwzEbU8R8rq9bqUFryO50RFBlkfufV9UfMArHPWlo+lmsC+NlSluHQALoj6Jkq3zf5ppn1CN0c1DDLrEqdQUXg==} engines: {node: '>=16.0.0'} + '@aws-sdk/util-arn-parser@3.723.0': + resolution: {integrity: sha512-ZhEfvUwNliOQROcAk34WJWVYTlTa4694kSVhDSjW6lE1bMataPnIN8A0ycukEzBXmd8ZSoBcQLn6lKGl7XIJ5w==} + engines: {node: '>=18.0.0'} + '@aws-sdk/util-endpoints@3.686.0': resolution: {integrity: sha512-7msZE2oYl+6QYeeRBjlDgxQUhq/XRky3cXE0FqLFs2muLS7XSuQEXkpOXB3R782ygAP6JX0kmBxPTLurRTikZg==} engines: {node: '>=16.0.0'} + '@aws-sdk/util-format-url@3.734.0': + resolution: {integrity: sha512-TxZMVm8V4aR/QkW9/NhujvYpPZjUYqzLwSge5imKZbWFR806NP7RMwc5ilVuHF/bMOln/cVHkl42kATElWBvNw==} + engines: {node: '>=18.0.0'} + '@aws-sdk/util-locate-window@3.679.0': resolution: {integrity: sha512-zKTd48/ZWrCplkXpYDABI74rQlbR0DNHs8nH95htfSLj9/mWRSwaGptoxwcihaq/77vi/fl2X3y0a1Bo8bt7RA==} engines: {node: '>=16.0.0'} @@ -3655,79 +3701,67 @@ packages: resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} @@ -4043,49 +4077,42 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@napi-rs/nice-linux-arm64-musl@1.0.1': resolution: {integrity: sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@napi-rs/nice-linux-ppc64-gnu@1.0.1': resolution: {integrity: sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==} engines: {node: '>= 10'} cpu: [ppc64] os: [linux] - libc: [glibc] '@napi-rs/nice-linux-riscv64-gnu@1.0.1': resolution: {integrity: sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - libc: [glibc] '@napi-rs/nice-linux-s390x-gnu@1.0.1': resolution: {integrity: sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==} engines: {node: '>= 10'} cpu: [s390x] os: [linux] - libc: [glibc] '@napi-rs/nice-linux-x64-gnu@1.0.1': resolution: {integrity: sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@napi-rs/nice-linux-x64-musl@1.0.1': resolution: {integrity: sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@napi-rs/nice-win32-arm64-msvc@1.0.1': resolution: {integrity: sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==} @@ -4174,84 +4201,72 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-gnu@15.1.3': resolution: {integrity: sha512-YbdaYjyHa4fPK4GR4k2XgXV0p8vbU1SZh7vv6El4bl9N+ZSiMfbmqCuCuNU1Z4ebJMumafaz6UCC2zaJCsdzjw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-gnu@15.1.5': resolution: {integrity: sha512-rDJC4ctlYbK27tCyFUhgIv8o7miHNlpCjb2XXfTLQszwAUOSbcMN9q2y3urSrrRCyGVOd9ZR9a4S45dRh6JF3A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@15.0.3': resolution: {integrity: sha512-WkAk6R60mwDjH4lG/JBpb2xHl2/0Vj0ZRu1TIzWuOYfQ9tt9NFsIinI1Epma77JVgy81F32X/AeD+B2cBu/YQA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-arm64-musl@15.1.3': resolution: {integrity: sha512-qgH/aRj2xcr4BouwKG3XdqNu33SDadqbkqB6KaZZkozar857upxKakbRllpqZgWl/NDeSCBYPmUAZPBHZpbA0w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-arm64-musl@15.1.5': resolution: {integrity: sha512-FG5RApf4Gu+J+pHUQxXPM81oORZrKBYKUaBTylEIQ6Lz17hKVDsLbSXInfXM0giclvXbyiLXjTv42sQMATmZ0A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@15.0.3': resolution: {integrity: sha512-gWL/Cta1aPVqIGgDb6nxkqy06DkwJ9gAnKORdHWX1QBbSZZB+biFYPFti8aKIQL7otCE1pjyPaXpFzGeG2OS2w==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-gnu@15.1.3': resolution: {integrity: sha512-uzafnTFwZCPN499fNVnS2xFME8WLC9y7PLRs/yqz5lz1X/ySoxfaK2Hbz74zYUdEg+iDZPd8KlsWaw9HKkLEVw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-gnu@15.1.5': resolution: {integrity: sha512-NX2Ar3BCquAOYpnoYNcKz14eH03XuF7SmSlPzTSSU4PJe7+gelAjxo3Y7F2m8+hLT8ZkkqElawBp7SWBdzwqQw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@15.0.3': resolution: {integrity: sha512-QQEMwFd8r7C0GxQS62Zcdy6GKx999I/rTO2ubdXEe+MlZk9ZiinsrjwoiBL5/57tfyjikgh6GOU2WRQVUej3UA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-linux-x64-musl@15.1.3': resolution: {integrity: sha512-el6GUFi4SiDYnMTTlJJFMU+GHvw0UIFnffP1qhurrN1qJV3BqaSRUjkDUgVV44T6zpw1Lc6u+yn0puDKHs+Sbw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-linux-x64-musl@15.1.5': resolution: {integrity: sha512-EQgqMiNu3mrV5eQHOIgeuh6GB5UU57tu17iFnLfBEhYfiOfyK+vleYKh2dkRVkV6ayx3eSqbIYgE7J7na4hhcA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@15.0.3': resolution: {integrity: sha512-9TEp47AAd/ms9fPNgtgnT7F3M1Hf7koIYYWCMQ9neOwjbVWJsHZxrFbI3iEDJ8rf1TDGpmHbKxXf2IFpAvheIQ==} @@ -4538,25 +4553,21 @@ packages: resolution: {integrity: sha512-otVbS4zeo3n71zgGLBYRTriDzc0zpruC0WI3ICwjpIk454cLwGV0yzh4jlGYWQJYJk0BRAmXFd3ooKIF+bKBHw==} cpu: [arm64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-arm64-musl@1.12.0': resolution: {integrity: sha512-IStQDjIT7Lzmqg1i9wXvPL/NsYsxF24WqaQFS8b8rxra+z0VG7saBOsEnOaa4jcEY8MVpLYabFhTV+fSsA2vnA==} cpu: [arm64] os: [linux] - libc: [musl] '@oxc-resolver/binding-linux-x64-gnu@1.12.0': resolution: {integrity: sha512-SipT7EVORz8pOQSFwemOm91TpSiBAGmOjG830/o+aLEsvQ4pEy223+SAnCfITh7+AahldYsJnVoIs519jmIlKQ==} cpu: [x64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-x64-musl@1.12.0': resolution: {integrity: sha512-mGh0XfUzKdn+WFaqPacziNraCWL5znkHRfQVxG9avGS9zb2KC/N1EBbPzFqutDwixGDP54r2gx4q54YCJEZ4iQ==} cpu: [x64] os: [linux] - libc: [musl] '@oxc-resolver/binding-wasm32-wasi@1.12.0': resolution: {integrity: sha512-SZN6v7apKmQf/Vwiqb6e/s3Y2Oacw8uW8V2i1AlxtyaEFvnFE0UBn89zq6swEwE3OCajNWs0yPvgAXUMddYc7Q==} @@ -4795,6 +4806,10 @@ packages: resolution: {integrity: sha512-0XuhuHQlEqbNQZp7QxxrFTdVWdwxch4vjxYgfInF91hZFkPxf9QDrdQka0KfxFMPqLNzSw0b95uGTrLliQUavQ==} engines: {node: '>=16.0.0'} + '@smithy/abort-controller@4.0.1': + resolution: {integrity: sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g==} + engines: {node: '>=18.0.0'} + '@smithy/chunked-blob-reader-native@3.0.1': resolution: {integrity: sha512-VEYtPvh5rs/xlyqpm5NRnfYLZn+q0SRPELbvBV+C/G7IQ+ouTuo+NKKa3ShG5OaFR8NYVMXls9hPYLTvIKKDrQ==} @@ -4809,6 +4824,10 @@ packages: resolution: {integrity: sha512-DujtuDA7BGEKExJ05W5OdxCoyekcKT3Rhg1ZGeiUWaz2BJIWXjZmsG/DIP4W48GHno7AQwRsaCb8NcBgH3QZpg==} engines: {node: '>=16.0.0'} + '@smithy/core@3.1.4': + resolution: {integrity: sha512-wFExFGK+7r2wYriOqe7RRIBNpvxwiS95ih09+GSLRBdoyK/O1uZA7K7pKesj5CBvwJuSBeXwLyR88WwIAY+DGA==} + engines: {node: '>=18.0.0'} + '@smithy/credential-provider-imds@3.2.5': resolution: {integrity: sha512-4FTQGAsuwqTzVMmiRVTn0RR9GrbRfkP0wfu/tXWVHd2LgNpTY0uglQpIScXK4NaEyXbB3JmZt8gfVqO50lP8wg==} engines: {node: '>=16.0.0'} @@ -4835,6 +4854,10 @@ packages: '@smithy/fetch-http-handler@4.0.0': resolution: {integrity: sha512-MLb1f5tbBO2X6K4lMEKJvxeLooyg7guq48C2zKr4qM7F2Gpkz4dc+hdSgu77pCJ76jVqFBjZczHYAs6dp15N+g==} + '@smithy/fetch-http-handler@5.0.1': + resolution: {integrity: sha512-3aS+fP28urrMW2KTjb6z9iFow6jO8n3MFfineGbndvzGZit3taZhKWtTorf+Gp5RpFDDafeHlhfsGlDCXvUnJA==} + engines: {node: '>=18.0.0'} + '@smithy/hash-blob-browser@3.1.7': resolution: {integrity: sha512-4yNlxVNJifPM5ThaA5HKnHkn7JhctFUHvcaz6YXxHlYOSIrzI6VKQPTN8Gs1iN5nqq9iFcwIR9THqchUCouIfg==} @@ -4857,6 +4880,10 @@ packages: resolution: {integrity: sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==} engines: {node: '>=16.0.0'} + '@smithy/is-array-buffer@4.0.0': + resolution: {integrity: sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==} + engines: {node: '>=18.0.0'} + '@smithy/md5-js@3.0.8': resolution: {integrity: sha512-LwApfTK0OJ/tCyNUXqnWCKoE2b4rDSr4BJlDAVCkiWYeHESr+y+d5zlAanuLW6fnitVJRD/7d9/kN/ZM9Su4mA==} @@ -4868,6 +4895,10 @@ packages: resolution: {integrity: sha512-wWO3xYmFm6WRW8VsEJ5oU6h7aosFXfszlz3Dj176pTij6o21oZnzkCLzShfmRaaCHDkBXWBdO0c4sQAvLFP6zA==} engines: {node: '>=16.0.0'} + '@smithy/middleware-endpoint@4.0.5': + resolution: {integrity: sha512-cPzGZV7qStHwboFrm6GfrzQE+YDiCzWcTh4+7wKrP/ZQ4gkw+r7qDjV8GjM4N0UYsuUyLfpzLGg5hxsYTU11WA==} + engines: {node: '>=18.0.0'} + '@smithy/middleware-retry@3.0.25': resolution: {integrity: sha512-m1F70cPaMBML4HiTgCw5I+jFNtjgz5z5UdGnUbG37vw6kh4UvizFYjqJGHvicfgKMkDL6mXwyPp5mhZg02g5sg==} engines: {node: '>=16.0.0'} @@ -4876,34 +4907,66 @@ packages: resolution: {integrity: sha512-Xg2jK9Wc/1g/MBMP/EUn2DLspN8LNt+GMe7cgF+Ty3vl+Zvu+VeZU5nmhveU+H8pxyTsjrAkci8NqY6OuvZnjA==} engines: {node: '>=16.0.0'} + '@smithy/middleware-serde@4.0.2': + resolution: {integrity: sha512-Sdr5lOagCn5tt+zKsaW+U2/iwr6bI9p08wOkCp6/eL6iMbgdtc2R5Ety66rf87PeohR0ExI84Txz9GYv5ou3iQ==} + engines: {node: '>=18.0.0'} + '@smithy/middleware-stack@3.0.8': resolution: {integrity: sha512-d7ZuwvYgp1+3682Nx0MD3D/HtkmZd49N3JUndYWQXfRZrYEnCWYc8BHcNmVsPAp9gKvlurdg/mubE6b/rPS9MA==} engines: {node: '>=16.0.0'} + '@smithy/middleware-stack@4.0.1': + resolution: {integrity: sha512-dHwDmrtR/ln8UTHpaIavRSzeIk5+YZTBtLnKwDW3G2t6nAupCiQUvNzNoHBpik63fwUaJPtlnMzXbQrNFWssIA==} + engines: {node: '>=18.0.0'} + '@smithy/node-config-provider@3.1.9': resolution: {integrity: sha512-qRHoah49QJ71eemjuS/WhUXB+mpNtwHRWQr77J/m40ewBVVwvo52kYAmb7iuaECgGTTcYxHS4Wmewfwy++ueew==} engines: {node: '>=16.0.0'} + '@smithy/node-config-provider@4.0.1': + resolution: {integrity: sha512-8mRTjvCtVET8+rxvmzRNRR0hH2JjV0DFOmwXPrISmTIJEfnCBugpYYGAsCj8t41qd+RB5gbheSQ/6aKZCQvFLQ==} + engines: {node: '>=18.0.0'} + '@smithy/node-http-handler@3.2.5': resolution: {integrity: sha512-PkOwPNeKdvX/jCpn0A8n9/TyoxjGZB8WVoJmm9YzsnAgggTj4CrjpRHlTQw7dlLZ320n1mY1y+nTRUDViKi/3w==} engines: {node: '>=16.0.0'} + '@smithy/node-http-handler@4.0.2': + resolution: {integrity: sha512-X66H9aah9hisLLSnGuzRYba6vckuFtGE+a5DcHLliI/YlqKrGoxhisD5XbX44KyoeRzoNlGr94eTsMVHFAzPOw==} + engines: {node: '>=18.0.0'} + '@smithy/property-provider@3.1.8': resolution: {integrity: sha512-ukNUyo6rHmusG64lmkjFeXemwYuKge1BJ8CtpVKmrxQxc6rhUX0vebcptFA9MmrGsnLhwnnqeH83VTU9hwOpjA==} engines: {node: '>=16.0.0'} + '@smithy/property-provider@4.0.1': + resolution: {integrity: sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ==} + engines: {node: '>=18.0.0'} + '@smithy/protocol-http@4.1.5': resolution: {integrity: sha512-hsjtwpIemmCkm3ZV5fd/T0bPIugW1gJXwZ/hpuVubt2hEUApIoUTrf6qIdh9MAWlw0vjMrA1ztJLAwtNaZogvg==} engines: {node: '>=16.0.0'} + '@smithy/protocol-http@5.0.1': + resolution: {integrity: sha512-TE4cpj49jJNB/oHyh/cRVEgNZaoPaxd4vteJNB0yGidOCVR0jCw/hjPVsT8Q8FRmj8Bd3bFZt8Dh7xGCT+xMBQ==} + engines: {node: '>=18.0.0'} + '@smithy/querystring-builder@3.0.8': resolution: {integrity: sha512-btYxGVqFUARbUrN6VhL9c3dnSviIwBYD9Rz1jHuN1hgh28Fpv2xjU1HeCeDJX68xctz7r4l1PBnFhGg1WBBPuA==} engines: {node: '>=16.0.0'} + '@smithy/querystring-builder@4.0.1': + resolution: {integrity: sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg==} + engines: {node: '>=18.0.0'} + '@smithy/querystring-parser@3.0.8': resolution: {integrity: sha512-BtEk3FG7Ks64GAbt+JnKqwuobJNX8VmFLBsKIwWr1D60T426fGrV2L3YS5siOcUhhp6/Y6yhBw1PSPxA5p7qGg==} engines: {node: '>=16.0.0'} + '@smithy/querystring-parser@4.0.1': + resolution: {integrity: sha512-Ma2XC7VS9aV77+clSFylVUnPZRindhB7BbmYiNOdr+CHt/kZNJoPP0cd3QxCnCFyPXC4eybmyE98phEHkqZ5Jw==} + engines: {node: '>=18.0.0'} + '@smithy/service-error-classification@3.0.8': resolution: {integrity: sha512-uEC/kCCFto83bz5ZzapcrgGqHOh/0r69sZ2ZuHlgoD5kYgXJEThCoTuw/y1Ub3cE7aaKdznb+jD9xRPIfIwD7g==} engines: {node: '>=16.0.0'} @@ -4912,28 +4975,56 @@ packages: resolution: {integrity: sha512-/+OsJRNtoRbtsX0UpSgWVxFZLsJHo/4sTr+kBg/J78sr7iC+tHeOvOJrS5hCpVQ6sWBbhWLp1UNiuMyZhE6pmA==} engines: {node: '>=16.0.0'} + '@smithy/shared-ini-file-loader@4.0.1': + resolution: {integrity: sha512-hC8F6qTBbuHRI/uqDgqqi6J0R4GtEZcgrZPhFQnMhfJs3MnUTGSnR1NSJCJs5VWlMydu0kJz15M640fJlRsIOw==} + engines: {node: '>=18.0.0'} + '@smithy/signature-v4@4.2.1': resolution: {integrity: sha512-NsV1jF4EvmO5wqmaSzlnTVetemBS3FZHdyc5CExbDljcyJCEEkJr8ANu2JvtNbVg/9MvKAWV44kTrGS+Pi4INg==} engines: {node: '>=16.0.0'} + '@smithy/signature-v4@5.0.1': + resolution: {integrity: sha512-nCe6fQ+ppm1bQuw5iKoeJ0MJfz2os7Ic3GBjOkLOPtavbD1ONoyE3ygjBfz2ythFWm4YnRm6OxW+8p/m9uCoIA==} + engines: {node: '>=18.0.0'} + '@smithy/smithy-client@3.4.2': resolution: {integrity: sha512-dxw1BDxJiY9/zI3cBqfVrInij6ShjpV4fmGHesGZZUiP9OSE/EVfdwdRz0PgvkEvrZHpsj2htRaHJfftE8giBA==} engines: {node: '>=16.0.0'} + '@smithy/smithy-client@4.1.5': + resolution: {integrity: sha512-DMXYoYeL4QkElr216n1yodTFeATbfb4jwYM9gKn71Rw/FNA1/Sm36tkTSCsZEs7mgpG3OINmkxL9vgVFzyGPaw==} + engines: {node: '>=18.0.0'} + '@smithy/types@3.6.0': resolution: {integrity: sha512-8VXK/KzOHefoC65yRgCn5vG1cysPJjHnOVt9d0ybFQSmJgQj152vMn4EkYhGuaOmnnZvCPav/KnYyE6/KsNZ2w==} engines: {node: '>=16.0.0'} + '@smithy/types@4.1.0': + resolution: {integrity: sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw==} + engines: {node: '>=18.0.0'} + '@smithy/url-parser@3.0.8': resolution: {integrity: sha512-4FdOhwpTW7jtSFWm7SpfLGKIBC9ZaTKG5nBF0wK24aoQKQyDIKUw3+KFWCQ9maMzrgTJIuOvOnsV2lLGW5XjTg==} + '@smithy/url-parser@4.0.1': + resolution: {integrity: sha512-gPXcIEUtw7VlK8f/QcruNXm7q+T5hhvGu9tl63LsJPZ27exB6dtNwvh2HIi0v7JcXJ5emBxB+CJxwaLEdJfA+g==} + engines: {node: '>=18.0.0'} + '@smithy/util-base64@3.0.0': resolution: {integrity: sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==} engines: {node: '>=16.0.0'} + '@smithy/util-base64@4.0.0': + resolution: {integrity: sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==} + engines: {node: '>=18.0.0'} + '@smithy/util-body-length-browser@3.0.0': resolution: {integrity: sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==} + '@smithy/util-body-length-browser@4.0.0': + resolution: {integrity: sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==} + engines: {node: '>=18.0.0'} + '@smithy/util-body-length-node@3.0.0': resolution: {integrity: sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==} engines: {node: '>=16.0.0'} @@ -4946,10 +5037,18 @@ packages: resolution: {integrity: sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==} engines: {node: '>=16.0.0'} + '@smithy/util-buffer-from@4.0.0': + resolution: {integrity: sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==} + engines: {node: '>=18.0.0'} + '@smithy/util-config-provider@3.0.0': resolution: {integrity: sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==} engines: {node: '>=16.0.0'} + '@smithy/util-config-provider@4.0.0': + resolution: {integrity: sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==} + engines: {node: '>=18.0.0'} + '@smithy/util-defaults-mode-browser@3.0.25': resolution: {integrity: sha512-fRw7zymjIDt6XxIsLwfJfYUfbGoO9CmCJk6rjJ/X5cd20+d2Is7xjU5Kt/AiDt6hX8DAf5dztmfP5O82gR9emA==} engines: {node: '>= 10.0.0'} @@ -4966,10 +5065,18 @@ packages: resolution: {integrity: sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==} engines: {node: '>=16.0.0'} + '@smithy/util-hex-encoding@4.0.0': + resolution: {integrity: sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==} + engines: {node: '>=18.0.0'} + '@smithy/util-middleware@3.0.8': resolution: {integrity: sha512-p7iYAPaQjoeM+AKABpYWeDdtwQNxasr4aXQEA/OmbOaug9V0odRVDy3Wx4ci8soljE/JXQo+abV0qZpW8NX0yA==} engines: {node: '>=16.0.0'} + '@smithy/util-middleware@4.0.1': + resolution: {integrity: sha512-HiLAvlcqhbzhuiOa0Lyct5IIlyIz0PQO5dnMlmQ/ubYM46dPInB+3yQGkfxsk6Q24Y0n3/JmcA1v5iEhmOF5mA==} + engines: {node: '>=18.0.0'} + '@smithy/util-retry@3.0.8': resolution: {integrity: sha512-TCEhLnY581YJ+g1x0hapPz13JFqzmh/pMWL2KEFASC51qCfw3+Y47MrTmea4bUE5vsdxQ4F6/KFbUeSz22Q1ow==} engines: {node: '>=16.0.0'} @@ -4978,10 +5085,18 @@ packages: resolution: {integrity: sha512-R3ufuzJRxSJbE58K9AEnL/uSZyVdHzud9wLS8tIbXclxKzoe09CRohj2xV8wpx5tj7ZbiJaKYcutMm1eYgz/0A==} engines: {node: '>=16.0.0'} + '@smithy/util-stream@4.1.1': + resolution: {integrity: sha512-+Xvh8nhy0Wjv1y71rBVyV3eJU3356XsFQNI8dEZVNrQju7Eib8G31GWtO+zMa9kTCGd41Mflu+ZKfmQL/o2XzQ==} + engines: {node: '>=18.0.0'} + '@smithy/util-uri-escape@3.0.0': resolution: {integrity: sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==} engines: {node: '>=16.0.0'} + '@smithy/util-uri-escape@4.0.0': + resolution: {integrity: sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==} + engines: {node: '>=18.0.0'} + '@smithy/util-utf8@2.3.0': resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} engines: {node: '>=14.0.0'} @@ -4990,6 +5105,10 @@ packages: resolution: {integrity: sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==} engines: {node: '>=16.0.0'} + '@smithy/util-utf8@4.0.0': + resolution: {integrity: sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==} + engines: {node: '>=18.0.0'} + '@smithy/util-waiter@3.1.7': resolution: {integrity: sha512-d5yGlQtmN/z5eoTtIYgkvOw27US2Ous4VycnXatyoImIF9tzlcpnKqQ/V7qhvJmb2p6xZne1NopCLakdTnkBBQ==} engines: {node: '>=16.0.0'} @@ -5044,28 +5163,24 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [glibc] '@swc/core-linux-arm64-musl@1.10.12': resolution: {integrity: sha512-oqhSmV+XauSf0C//MoQnVErNUB/5OzmSiUzuazyLsD5pwqKNN+leC3JtRQ/QVzaCpr65jv9bKexT9+I2Tt3xDw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [musl] '@swc/core-linux-x64-gnu@1.10.12': resolution: {integrity: sha512-XldSIHyjD7m1Gh+/8rxV3Ok711ENLI420CU2EGEqSe3VSGZ7pHJvJn9ZFbYpWhsLxPqBYMFjp3Qw+J6OXCPXCA==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [glibc] '@swc/core-linux-x64-musl@1.10.12': resolution: {integrity: sha512-wvPXzJxzPgTqhyp1UskOx1hRTtdWxlyFD1cGWOxgLsMik0V9xKRgqKnMPv16Nk7L9xl6quQ6DuUHj9ID7L3oVw==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [musl] '@swc/core-win32-arm64-msvc@1.10.12': resolution: {integrity: sha512-TUYzWuu1O7uyIcRfxdm6Wh1u+gNnrW5M1DUgDOGZLsyQzgc2Zjwfh2llLhuAIilvCVg5QiGbJlpibRYJ/8QGsg==} @@ -5846,6 +5961,7 @@ packages: bson@6.10.1: resolution: {integrity: sha512-P92xmHDQjSKPLHqFxefqMxASNq/aWJMEZugpCjf+AF/pgcUpMMQCg7t7+ewko0/u8AapvF3luf/FoehddEK+sA==} engines: {node: '>=16.20.1'} + deprecated: a critical bug affecting only useBigInt64=true deserialization usage is fixed in bson@6.10.3 buffer-builder@0.2.0: resolution: {integrity: sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==} @@ -10554,6 +10670,20 @@ snapshots: fast-xml-parser: 4.4.1 tslib: 2.8.1 + '@aws-sdk/core@3.750.0': + dependencies: + '@aws-sdk/types': 3.734.0 + '@smithy/core': 3.1.4 + '@smithy/node-config-provider': 4.0.1 + '@smithy/property-provider': 4.0.1 + '@smithy/protocol-http': 5.0.1 + '@smithy/signature-v4': 5.0.1 + '@smithy/smithy-client': 4.1.5 + '@smithy/types': 4.1.0 + '@smithy/util-middleware': 4.0.1 + fast-xml-parser: 4.4.1 + tslib: 2.8.1 + '@aws-sdk/credential-provider-cognito-identity@3.687.0': dependencies: '@aws-sdk/client-cognito-identity': 3.687.0 @@ -10764,6 +10894,23 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.1 + '@aws-sdk/middleware-sdk-s3@3.750.0': + dependencies: + '@aws-sdk/core': 3.750.0 + '@aws-sdk/types': 3.734.0 + '@aws-sdk/util-arn-parser': 3.723.0 + '@smithy/core': 3.1.4 + '@smithy/node-config-provider': 4.0.1 + '@smithy/protocol-http': 5.0.1 + '@smithy/signature-v4': 5.0.1 + '@smithy/smithy-client': 4.1.5 + '@smithy/types': 4.1.0 + '@smithy/util-config-provider': 4.0.0 + '@smithy/util-middleware': 4.0.1 + '@smithy/util-stream': 4.1.1 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + '@aws-sdk/middleware-ssec@3.686.0': dependencies: '@aws-sdk/types': 3.686.0 @@ -10789,6 +10936,17 @@ snapshots: '@smithy/util-middleware': 3.0.8 tslib: 2.8.1 + '@aws-sdk/s3-request-presigner@3.750.0': + dependencies: + '@aws-sdk/signature-v4-multi-region': 3.750.0 + '@aws-sdk/types': 3.734.0 + '@aws-sdk/util-format-url': 3.734.0 + '@smithy/middleware-endpoint': 4.0.5 + '@smithy/protocol-http': 5.0.1 + '@smithy/smithy-client': 4.1.5 + '@smithy/types': 4.1.0 + tslib: 2.8.1 + '@aws-sdk/signature-v4-multi-region@3.687.0': dependencies: '@aws-sdk/middleware-sdk-s3': 3.687.0 @@ -10798,6 +10956,15 @@ snapshots: '@smithy/types': 3.6.0 tslib: 2.8.1 + '@aws-sdk/signature-v4-multi-region@3.750.0': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.750.0 + '@aws-sdk/types': 3.734.0 + '@smithy/protocol-http': 5.0.1 + '@smithy/signature-v4': 5.0.1 + '@smithy/types': 4.1.0 + tslib: 2.8.1 + '@aws-sdk/token-providers@3.686.0(@aws-sdk/client-sso-oidc@3.687.0(@aws-sdk/client-sts@3.687.0))': dependencies: '@aws-sdk/client-sso-oidc': 3.687.0(@aws-sdk/client-sts@3.687.0) @@ -10812,10 +10979,19 @@ snapshots: '@smithy/types': 3.6.0 tslib: 2.8.1 + '@aws-sdk/types@3.734.0': + dependencies: + '@smithy/types': 4.1.0 + tslib: 2.8.1 + '@aws-sdk/util-arn-parser@3.679.0': dependencies: tslib: 2.8.1 + '@aws-sdk/util-arn-parser@3.723.0': + dependencies: + tslib: 2.8.1 + '@aws-sdk/util-endpoints@3.686.0': dependencies: '@aws-sdk/types': 3.686.0 @@ -10823,6 +10999,13 @@ snapshots: '@smithy/util-endpoints': 2.1.4 tslib: 2.8.1 + '@aws-sdk/util-format-url@3.734.0': + dependencies: + '@aws-sdk/types': 3.734.0 + '@smithy/querystring-builder': 4.0.1 + '@smithy/types': 4.1.0 + tslib: 2.8.1 + '@aws-sdk/util-locate-window@3.679.0': dependencies: tslib: 2.8.1 @@ -13871,6 +14054,11 @@ snapshots: '@smithy/types': 3.6.0 tslib: 2.8.1 + '@smithy/abort-controller@4.0.1': + dependencies: + '@smithy/types': 4.1.0 + tslib: 2.8.1 + '@smithy/chunked-blob-reader-native@3.0.1': dependencies: '@smithy/util-base64': 3.0.0 @@ -13899,6 +14087,17 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.1 + '@smithy/core@3.1.4': + dependencies: + '@smithy/middleware-serde': 4.0.2 + '@smithy/protocol-http': 5.0.1 + '@smithy/types': 4.1.0 + '@smithy/util-body-length-browser': 4.0.0 + '@smithy/util-middleware': 4.0.1 + '@smithy/util-stream': 4.1.1 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + '@smithy/credential-provider-imds@3.2.5': dependencies: '@smithy/node-config-provider': 3.1.9 @@ -13945,6 +14144,14 @@ snapshots: '@smithy/util-base64': 3.0.0 tslib: 2.8.1 + '@smithy/fetch-http-handler@5.0.1': + dependencies: + '@smithy/protocol-http': 5.0.1 + '@smithy/querystring-builder': 4.0.1 + '@smithy/types': 4.1.0 + '@smithy/util-base64': 4.0.0 + tslib: 2.8.1 + '@smithy/hash-blob-browser@3.1.7': dependencies: '@smithy/chunked-blob-reader': 4.0.0 @@ -13978,6 +14185,10 @@ snapshots: dependencies: tslib: 2.8.1 + '@smithy/is-array-buffer@4.0.0': + dependencies: + tslib: 2.8.1 + '@smithy/md5-js@3.0.8': dependencies: '@smithy/types': 3.6.0 @@ -14001,6 +14212,17 @@ snapshots: '@smithy/util-middleware': 3.0.8 tslib: 2.8.1 + '@smithy/middleware-endpoint@4.0.5': + dependencies: + '@smithy/core': 3.1.4 + '@smithy/middleware-serde': 4.0.2 + '@smithy/node-config-provider': 4.0.1 + '@smithy/shared-ini-file-loader': 4.0.1 + '@smithy/types': 4.1.0 + '@smithy/url-parser': 4.0.1 + '@smithy/util-middleware': 4.0.1 + tslib: 2.8.1 + '@smithy/middleware-retry@3.0.25': dependencies: '@smithy/node-config-provider': 3.1.9 @@ -14018,11 +14240,21 @@ snapshots: '@smithy/types': 3.6.0 tslib: 2.8.1 + '@smithy/middleware-serde@4.0.2': + dependencies: + '@smithy/types': 4.1.0 + tslib: 2.8.1 + '@smithy/middleware-stack@3.0.8': dependencies: '@smithy/types': 3.6.0 tslib: 2.8.1 + '@smithy/middleware-stack@4.0.1': + dependencies: + '@smithy/types': 4.1.0 + tslib: 2.8.1 + '@smithy/node-config-provider@3.1.9': dependencies: '@smithy/property-provider': 3.1.8 @@ -14030,6 +14262,13 @@ snapshots: '@smithy/types': 3.6.0 tslib: 2.8.1 + '@smithy/node-config-provider@4.0.1': + dependencies: + '@smithy/property-provider': 4.0.1 + '@smithy/shared-ini-file-loader': 4.0.1 + '@smithy/types': 4.1.0 + tslib: 2.8.1 + '@smithy/node-http-handler@3.2.5': dependencies: '@smithy/abort-controller': 3.1.6 @@ -14038,27 +14277,56 @@ snapshots: '@smithy/types': 3.6.0 tslib: 2.8.1 + '@smithy/node-http-handler@4.0.2': + dependencies: + '@smithy/abort-controller': 4.0.1 + '@smithy/protocol-http': 5.0.1 + '@smithy/querystring-builder': 4.0.1 + '@smithy/types': 4.1.0 + tslib: 2.8.1 + '@smithy/property-provider@3.1.8': dependencies: '@smithy/types': 3.6.0 tslib: 2.8.1 + '@smithy/property-provider@4.0.1': + dependencies: + '@smithy/types': 4.1.0 + tslib: 2.8.1 + '@smithy/protocol-http@4.1.5': dependencies: '@smithy/types': 3.6.0 tslib: 2.8.1 + '@smithy/protocol-http@5.0.1': + dependencies: + '@smithy/types': 4.1.0 + tslib: 2.8.1 + '@smithy/querystring-builder@3.0.8': dependencies: '@smithy/types': 3.6.0 '@smithy/util-uri-escape': 3.0.0 tslib: 2.8.1 + '@smithy/querystring-builder@4.0.1': + dependencies: + '@smithy/types': 4.1.0 + '@smithy/util-uri-escape': 4.0.0 + tslib: 2.8.1 + '@smithy/querystring-parser@3.0.8': dependencies: '@smithy/types': 3.6.0 tslib: 2.8.1 + '@smithy/querystring-parser@4.0.1': + dependencies: + '@smithy/types': 4.1.0 + tslib: 2.8.1 + '@smithy/service-error-classification@3.0.8': dependencies: '@smithy/types': 3.6.0 @@ -14068,6 +14336,11 @@ snapshots: '@smithy/types': 3.6.0 tslib: 2.8.1 + '@smithy/shared-ini-file-loader@4.0.1': + dependencies: + '@smithy/types': 4.1.0 + tslib: 2.8.1 + '@smithy/signature-v4@4.2.1': dependencies: '@smithy/is-array-buffer': 3.0.0 @@ -14079,6 +14352,17 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.1 + '@smithy/signature-v4@5.0.1': + dependencies: + '@smithy/is-array-buffer': 4.0.0 + '@smithy/protocol-http': 5.0.1 + '@smithy/types': 4.1.0 + '@smithy/util-hex-encoding': 4.0.0 + '@smithy/util-middleware': 4.0.1 + '@smithy/util-uri-escape': 4.0.0 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + '@smithy/smithy-client@3.4.2': dependencies: '@smithy/core': 2.5.1 @@ -14089,26 +14373,56 @@ snapshots: '@smithy/util-stream': 3.2.1 tslib: 2.8.1 + '@smithy/smithy-client@4.1.5': + dependencies: + '@smithy/core': 3.1.4 + '@smithy/middleware-endpoint': 4.0.5 + '@smithy/middleware-stack': 4.0.1 + '@smithy/protocol-http': 5.0.1 + '@smithy/types': 4.1.0 + '@smithy/util-stream': 4.1.1 + tslib: 2.8.1 + '@smithy/types@3.6.0': dependencies: tslib: 2.8.1 + '@smithy/types@4.1.0': + dependencies: + tslib: 2.8.1 + '@smithy/url-parser@3.0.8': dependencies: '@smithy/querystring-parser': 3.0.8 '@smithy/types': 3.6.0 tslib: 2.8.1 + '@smithy/url-parser@4.0.1': + dependencies: + '@smithy/querystring-parser': 4.0.1 + '@smithy/types': 4.1.0 + tslib: 2.8.1 + '@smithy/util-base64@3.0.0': dependencies: '@smithy/util-buffer-from': 3.0.0 '@smithy/util-utf8': 3.0.0 tslib: 2.8.1 + '@smithy/util-base64@4.0.0': + dependencies: + '@smithy/util-buffer-from': 4.0.0 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + '@smithy/util-body-length-browser@3.0.0': dependencies: tslib: 2.8.1 + '@smithy/util-body-length-browser@4.0.0': + dependencies: + tslib: 2.8.1 + '@smithy/util-body-length-node@3.0.0': dependencies: tslib: 2.8.1 @@ -14123,10 +14437,19 @@ snapshots: '@smithy/is-array-buffer': 3.0.0 tslib: 2.8.1 + '@smithy/util-buffer-from@4.0.0': + dependencies: + '@smithy/is-array-buffer': 4.0.0 + tslib: 2.8.1 + '@smithy/util-config-provider@3.0.0': dependencies: tslib: 2.8.1 + '@smithy/util-config-provider@4.0.0': + dependencies: + tslib: 2.8.1 + '@smithy/util-defaults-mode-browser@3.0.25': dependencies: '@smithy/property-provider': 3.1.8 @@ -14155,11 +14478,20 @@ snapshots: dependencies: tslib: 2.8.1 + '@smithy/util-hex-encoding@4.0.0': + dependencies: + tslib: 2.8.1 + '@smithy/util-middleware@3.0.8': dependencies: '@smithy/types': 3.6.0 tslib: 2.8.1 + '@smithy/util-middleware@4.0.1': + dependencies: + '@smithy/types': 4.1.0 + tslib: 2.8.1 + '@smithy/util-retry@3.0.8': dependencies: '@smithy/service-error-classification': 3.0.8 @@ -14177,10 +14509,25 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.1 + '@smithy/util-stream@4.1.1': + dependencies: + '@smithy/fetch-http-handler': 5.0.1 + '@smithy/node-http-handler': 4.0.2 + '@smithy/types': 4.1.0 + '@smithy/util-base64': 4.0.0 + '@smithy/util-buffer-from': 4.0.0 + '@smithy/util-hex-encoding': 4.0.0 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + '@smithy/util-uri-escape@3.0.0': dependencies: tslib: 2.8.1 + '@smithy/util-uri-escape@4.0.0': + dependencies: + tslib: 2.8.1 + '@smithy/util-utf8@2.3.0': dependencies: '@smithy/util-buffer-from': 2.2.0 @@ -14191,6 +14538,11 @@ snapshots: '@smithy/util-buffer-from': 3.0.0 tslib: 2.8.1 + '@smithy/util-utf8@4.0.0': + dependencies: + '@smithy/util-buffer-from': 4.0.0 + tslib: 2.8.1 + '@smithy/util-waiter@3.1.7': dependencies: '@smithy/abort-controller': 3.1.6 diff --git a/test/storage-uploadthing/config.ts b/test/storage-uploadthing/config.ts index 871644e49..c5c66222b 100644 --- a/test/storage-uploadthing/config.ts +++ b/test/storage-uploadthing/config.ts @@ -36,6 +36,7 @@ export default buildConfigWithDefaults({ }, plugins: [ uploadthingStorage({ + clientUploads: true, collections: { [mediaSlug]: true, }, diff --git a/tsconfig.base.json b/tsconfig.base.json index ffd7ec771..26c3c9821 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -66,7 +66,15 @@ "./packages/plugin-multi-tenant/src/exports/client.ts" ], "@payloadcms/plugin-multi-tenant": ["./packages/plugin-multi-tenant/src/index.ts"], - "@payloadcms/next": ["./packages/next/src/exports/*"] + "@payloadcms/next": ["./packages/next/src/exports/*"], + "@payloadcms/storage-s3/client": ["./packages/storage-s3/src/exports/client.ts"], + "@payloadcms/storage-vercel-blob/client": [ + "./packages/storage-vercel-blob/src/exports/client.ts" + ], + "@payloadcms/storage-gcs/client": ["./packages/storage-gcs/src/exports/client.ts"], + "@payloadcms/storage-uploadthing/client": [ + "./packages/storage-uploadthing/src/exports/client.ts" + ] } }, "include": ["${configDir}/src"], diff --git a/tsconfig.json b/tsconfig.json index a9f8afbb3..33c6a6c04 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -40,6 +40,9 @@ { "path": "./packages/plugin-cloud-storage" }, + { + "path": "./packages/storage-s3" + }, { "path": "./packages/payload-cloud" },