obsidian/wiki/payloadcms/upload.md
2026-05-15 15:13:56 +01:00

249 lines
7.3 KiB
Markdown

---
tags: [payloadcms, tech-patterns]
topic: payloadcms
sources: [upload__overview.md, upload__storage-adapters.md]
created: 2026-05-15
---
# PayloadCMS — Upload & Media
## Overview
Enabling `upload` on a collection transforms it into a full file management system. Payload auto-injects `filename`, `mimeType`, `filesize` (and optionally `sizes`) fields and modifies the Admin Panel list/edit views to support file operations.
Common use cases:
- Media library for site images
- Gated content (PDFs, ebooks behind auth)
- Publicly downloadable assets (ZIPs, MP4s)
Upload only works via REST and Local APIs — not GraphQL.
## Setup / Config
Minimal collection setup:
```ts
import type { CollectionConfig } from 'payload'
export const Media: CollectionConfig = {
slug: 'media',
upload: {
staticDir: 'media', // relative to config file
mimeTypes: ['image/*'],
adminThumbnail: 'thumbnail',
imageSizes: [
{ name: 'thumbnail', width: 400, height: 300, position: 'centre' },
{ name: 'card', width: 768, height: 1024, position: 'centre' },
{ name: 'tablet', width: 1024, height: undefined, position: 'centre' },
],
},
fields: [{ name: 'alt', type: 'text' }],
}
```
Global file size limit (Busboy options):
```ts
export default buildConfig({
upload: {
limits: { fileSize: 5_000_000 }, // 5 MB in bytes
},
})
```
## Key Options
### Collection-level `upload` config
| Option | Notes |
|---|---|
| `staticDir` | Local storage folder; defaults to collection slug |
| `imageSizes` | Array of resize targets (uses `sharp`) |
| `adminThumbnail` | String (size name) or `({ doc }) => url` function |
| `mimeTypes` | Array — restricts file picker: `['image/*', 'application/pdf']` |
| `disableLocalStorage` | Set `true` when using a cloud storage adapter |
| `crop` / `focalPoint` | Both `true` by default; set `false` to disable UI tools |
| `resizeOptions` | Single sharp resize (alternative to `imageSizes`) |
| `formatOptions` | Sharp output format override |
| `pasteURL` | `false` to disable URL paste; object with `allowList` for server-side CORS fetch |
| `bulkUpload` | `true` by default |
| `filesRequiredOnCreate` | `true` by default |
| `withMetadata` | Append EXIF metadata to output image |
| `modifyResponseHeaders` | Manipulate HTTP response headers for served files |
| `allowRestrictedFileTypes` | `true` to bypass blocked executable/script extension list |
### Image sizes — `withoutEnlargement`
- `undefined` (default) — returns `null` when upload is smaller than defined size
- `false` — always enlarge
- `true` — return original image when smaller
### Custom filename via `beforeOperation` hook
```ts
beforeOperation: [
({ req, operation }) => {
if ((operation === 'create' || operation === 'update') && req.file) {
req.file.name = 'custom-name.jpg'
}
},
],
```
Custom per-size filename:
```ts
{
name: 'thumbnail',
width: 400, height: 300,
generateImageName: ({ height, sizeName, extension, width }) =>
`custom-${sizeName}-${height}-${width}.${extension}`,
}
```
## Code Examples
### Upload via REST (browser)
```ts
const formData = new FormData()
formData.append('file', fileInput.files[0])
formData.append('_payload', JSON.stringify({ alt: 'My image' }))
// Do NOT set Content-Type manually — browser sets multipart boundary
fetch('/api/media', { method: 'POST', body: formData })
```
### Upload from local path (seed scripts)
```ts
await payload.create({
collection: 'media',
data: { alt: 'Seeded image' },
filePath: path.resolve(__dirname, 'seed/image.jpg'),
})
```
### Restrict paste-URL to trusted domains
```ts
upload: {
pasteURL: {
allowList: [
{ hostname: 'assets.example.com', protocol: 'https', pathname: '/images/*' },
],
},
}
```
### Custom admin thumbnail via function
```ts
upload: {
adminThumbnail: ({ doc }) =>
`https://cdn.example.com/${doc.filename}?w=200`,
}
```
## Storage Adapters
All adapters auto-set `disableLocalStorage: true` per collection.
| Service | Package |
|---|---|
| Vercel Blob | `@payloadcms/storage-vercel-blob` |
| AWS S3 | `@payloadcms/storage-s3` |
| Azure Blob | `@payloadcms/storage-azure` |
| GCS | `@payloadcms/storage-gcs` |
| Uploadthing | `@payloadcms/storage-uploadthing` |
| R2 (Workers) | `@payloadcms/storage-r2` |
S3 example (also works for Cloudflare R2 via S3 API):
```ts
import { s3Storage } from '@payloadcms/storage-s3'
plugins: [
s3Storage({
collections: { media: true },
bucket: process.env.S3_BUCKET,
config: {
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
},
region: process.env.S3_REGION,
},
}),
]
```
Cloudflare R2 via S3 API (Node.js / Vercel):
```ts
s3Storage({
enabled: Boolean(process.env.R2_BUCKET),
collections: {
media: {
disablePayloadAccessControl: true,
generateFileURL: ({ filename, prefix }) => {
const key = prefix ? `${prefix}/${filename}` : filename
return `${process.env.R2_PUBLIC_URL}/${key}`
},
},
},
bucket: process.env.R2_BUCKET,
config: {
credentials: { accessKeyId: process.env.R2_ACCESS_KEY_ID, secretAccessKey: process.env.R2_SECRET_ACCESS_KEY },
region: 'auto', // required by R2
endpoint: process.env.R2_ENDPOINT,
forcePathStyle: true, // required for R2
},
})
```
### Prefix composition
By default a document-level prefix overrides the collection prefix. Use `useCompositePrefixes: true` to combine them:
```
Collection prefix: uploads + Document prefix: user-123
Default → user-123/file.jpg
Composite → uploads/user-123/file.jpg
```
### Payload Access Control for cloud files
By default file URLs stay at `/collectionSlug/staticURL/filename` so access control still applies. Set `disablePayloadAccessControl: true` only when your collection uses `read: () => true` and you want direct CDN URLs.
### Custom storage adapter (base plugin)
```ts
import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage'
plugins: [
cloudStoragePlugin({
collections: {
'my-collection': { adapter: myCustomAdapter },
},
}),
]
```
`GeneratedAdapter` interface requires: `handleDelete`, `handleUpload`, `staticHandler`, `name`.
## Gotchas
- `sharp` must be configured in Payload config for image resizing to work (auto-configured by `create-payload-app`)
- `withoutEnlargement` defaults to `undefined` — images smaller than a defined size return `null` for that size, not the original
- Image cropping happens **before** resizing — resized sizes are derived from the cropped image
- Focal point selector only shows up when `imageSizes` or `resizeOptions` is defined
- Vercel server uploads are limited to 4.5 MB — set `clientUploads: true` on the adapter to bypass
- R2: the S3 API endpoint is for uploads only; serve files via `R2_PUBLIC_URL` (r2.dev subdomain or custom domain)
- Restricted file types (`.exe`, `.php`, `.js`, HTML, scripts) are blocked by default unless `mimeTypes` is set or `allowRestrictedFileTypes: true`
- Custom upload components must wrap Payload's `<Upload>` from `@payloadcms/ui` — a raw `<input type="file">` won't connect to the form state and causes 400 errors
## Related
- [[wiki/payloadcms/configuration|Configuration]]
- [[wiki/payloadcms/fields-complex|Fields Complex]]
- [[wiki/payloadcms/admin-panel-overview|Admin Panel Overview]]