feat: disambiguate media files by prefix via query parameter#15844
Merged
DanRibbens merged 16 commits intopayloadcms:mainfrom Apr 7, 2026
Merged
feat: disambiguate media files by prefix via query parameter#15844DanRibbens merged 16 commits intopayloadcms:mainfrom
DanRibbens merged 16 commits intopayloadcms:mainfrom
Conversation
Add optional `?prefix=` query string support to the existing `/api/:collection/file/:filename` endpoint to disambiguate files that share the same filename but have different storage prefixes. When a storage prefix (e.g., UUID or folder path) is used to make S3 keys unique, multiple documents can share the same `filename`. The current endpoint resolves by filename alone, which can match the wrong document in both access control and file retrieval. - `checkFileAccess` accepts optional `prefix` and adds it to the `where` clause alongside existing access filters - `getFile` handler reads `prefix` from `req.searchParams` and threads it to `checkFileAccess` and handler `params` - `getFilePrefix` accepts `explicitPrefix` to skip the DB query when the prefix is already known from the URL - S3 `staticHandler` forwards `prefix` from params to `getFilePrefix` - `afterRead` hook appends `?prefix=` to Payload-proxied URLs - Added unit tests for `checkFileAccess` and `getFilePrefix`, and integration tests for the endpoint'
tschwartz
commented
Mar 4, 2026
56ba0eb to
170b95e
Compare
When a media URL already contains query parameters, appending the imageCacheTag with '?' produces a malformed URL with double '?'. Check for existing query parameters and use '&' as the separator when needed, matching the pattern already used in the Thumbnail component.
a0c9916 to
de5df42
Compare
GermanJablo
previously approved these changes
Mar 10, 2026
Contributor
GermanJablo
left a comment
There was a problem hiding this comment.
One minor note (non-blocking)
Only the S3 storage adapter is updated. Other storage adapters (Azure, GCS, Vercel Blob, etc.) aren't changed. This is fine since the core changes (checkFileAccess, getFile) work independently. Other adapters can be updated separately.
Update storage-vercel-blob, storage-azure, storage-gcs, and storage-r2 static handlers to destructure `prefix` from params and pass it to `getFilePrefix` as `explicitPrefix`, matching the pattern established in the S3 adapter. This avoids an unnecessary DB query when the prefix is already known from the `?prefix=` URL query parameter. - storage-vercel-blob: add `prefix: explicitPrefix` to params and `getFilePrefix` call - storage-azure: same - storage-gcs: same - storage-r2: import `getFilePrefix`, replace static prefix arg with dynamic lookup, fall back to constructor default - Update URL assertions in storage-azure, storage-s3, and plugin-cloud-storage integration tests to expect `?prefix=` query parameter on prefix-enabled collection URLs
Add optional `?prefix=` query string support to the existing `/api/:collection/file/:filename` endpoint to disambiguate files that share the same filename but have different storage prefixes. When a storage prefix (e.g., UUID or folder path) is used to make S3 keys unique, multiple documents can share the same `filename`. The current endpoint resolves by filename alone, which can match the wrong document in both access control and file retrieval. - `checkFileAccess` accepts optional `prefix` and adds it to the `where` clause alongside existing access filters - `getFile` handler reads `prefix` from `req.searchParams` and threads it to `checkFileAccess` and handler `params` - `getFilePrefix` accepts `explicitPrefix` to skip the DB query when the prefix is already known from the URL - S3 `staticHandler` forwards `prefix` from params to `getFilePrefix` - `afterRead` hook appends `?prefix=` to Payload-proxied URLs - Added unit tests for `checkFileAccess` and `getFilePrefix`, and integration tests for the endpoint'
When a media URL already contains query parameters, appending the imageCacheTag with '?' produces a malformed URL with double '?'. Check for existing query parameters and use '&' as the separator when needed, matching the pattern already used in the Thumbnail component.
Update storage-vercel-blob, storage-azure, storage-gcs, and storage-r2 static handlers to destructure `prefix` from params and pass it to `getFilePrefix` as `explicitPrefix`, matching the pattern established in the S3 adapter. This avoids an unnecessary DB query when the prefix is already known from the `?prefix=` URL query parameter. - storage-vercel-blob: add `prefix: explicitPrefix` to params and `getFilePrefix` call - storage-azure: same - storage-gcs: same - storage-r2: import `getFilePrefix`, replace static prefix arg with dynamic lookup, fall back to constructor default - Update URL assertions in storage-azure, storage-s3, and plugin-cloud-storage integration tests to expect `?prefix=` query parameter on prefix-enabled collection URLs
ec48dd0 to
1a04aef
Compare
…into media-endpoint
DanRibbens
approved these changes
Apr 7, 2026
Contributor
|
🚀 This is included in version v3.82.0 |
JarrodMFlesch
added a commit
that referenced
this pull request
Apr 10, 2026
## Summary
- Replace factory function pattern (`getHandleUpload`,
`getHandleDelete`, etc.) with simple helper functions
- Create `adapter.ts` in each storage package that maps the plugin
interface to the helpers
- Fix R2 prefix bug: use OR logic (`filePrefix || prefix`) to match
`handleUpload` behavior
- Remove redundant `async/await` on pass-through handlers
I think this approach makes it easier to trace backwards to the
cloud-storage plugin the arguments that are actually passed into the
adapter methods.
## Changes
**Pattern change (all 6 storage adapters):**
Before (factory functions):
```typescript
export const getHandleDelete = ({ bucket }: Args): HandleDelete => {
return async ({ doc: { prefix = '' }, filename }) => {
await bucket.delete(path.posix.join(prefix, filename))
}
}
```
After (simple functions + adapter mapping):
```typescript
// handleDelete.ts
export async function deleteFile({ bucket, prefix, filename }: DeleteArgs): Promise<void> {
await bucket.delete(path.posix.join(prefix, filename))
}
// adapter.ts
export function createR2Adapter({ bucket, clientUploads }: CreateR2AdapterArgs): Adapter {
return ({ collection, prefix = '' }): GeneratedAdapter => ({
name: 'r2',
clientUploads,
handleDelete: ({ doc: { prefix: docPrefix = '' }, filename }) =>
deleteFile({ bucket, filename, prefix: docPrefix }),
handleUpload: ({ data, file }) =>
uploadFile({
bucket,
buffer: file.buffer,
filename: file.filename,
mimeType: file.mimeType,
prefix: data.prefix || prefix,
}),
staticHandler: (
req,
{ headers, params: { clientUploadContext, filename, prefix: prefixQueryParam } },
) =>
getFile({
bucket,
clientUploadContext,
collection,
filename,
incomingHeaders: headers,
prefix,
prefixQueryParam,
req,
}),
})
}
```
**Bug fix (storage-r2):**
The prefix query param feature (#15844) introduced inconsistent logic in
R2:
- `handleUpload`: uses `data.prefix || prefix` (OR logic)
- `staticHandler`: was using `prefix + filePrefix` (AND logic)
Fixed to use consistent OR logic: `filePrefix || prefix`
## Packages affected
- `@payloadcms/storage-r2`
- `@payloadcms/storage-s3`
- `@payloadcms/storage-gcs`
- `@payloadcms/storage-azure`
- `@payloadcms/storage-vercel-blob`
- `@payloadcms/storage-uploadthing`
|
This diff caused me some trouble. Our app transforms the returned media path without checking for query params, to point to dirs where our m3u8 files live. Due to there suddenly being a query param in the url it breaks. I've had to revert a deploy and am now checking how to unset the query param without disabling the rest of the prefix logic. |
milamer
pushed a commit
to milamer/payload
that referenced
this pull request
Apr 20, 2026
…cms#15844) Add optional `?prefix=` query string support to the existing `/api/:collection/file/:filename` endpoint to disambiguate files that share the same filename but have different storage prefixes. When a storage prefix (e.g., UUID or folder path) is used to make S3 keys unique, multiple documents can share the same `filename`. The current endpoint resolves by filename alone, which can match the wrong document in both access control and file retrieval. - `checkFileAccess` accepts optional `prefix` and adds it to the `where` clause alongside existing access filters - `getFile` handler reads `prefix` from `req.searchParams` and threads it to `checkFileAccess` and handler `params` - `getFilePrefix` accepts `explicitPrefix` to skip the DB query when the prefix is already known from the URL - S3 `staticHandler` forwards `prefix` from params to `getFilePrefix` - `afterRead` hook appends `?prefix=` to Payload-proxied URLs - Added unit tests for `checkFileAccess` and `getFilePrefix`, and integration tests for the endpoint' --------- Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
milamer
pushed a commit
to milamer/payload
that referenced
this pull request
Apr 20, 2026
## Summary
- Replace factory function pattern (`getHandleUpload`,
`getHandleDelete`, etc.) with simple helper functions
- Create `adapter.ts` in each storage package that maps the plugin
interface to the helpers
- Fix R2 prefix bug: use OR logic (`filePrefix || prefix`) to match
`handleUpload` behavior
- Remove redundant `async/await` on pass-through handlers
I think this approach makes it easier to trace backwards to the
cloud-storage plugin the arguments that are actually passed into the
adapter methods.
## Changes
**Pattern change (all 6 storage adapters):**
Before (factory functions):
```typescript
export const getHandleDelete = ({ bucket }: Args): HandleDelete => {
return async ({ doc: { prefix = '' }, filename }) => {
await bucket.delete(path.posix.join(prefix, filename))
}
}
```
After (simple functions + adapter mapping):
```typescript
// handleDelete.ts
export async function deleteFile({ bucket, prefix, filename }: DeleteArgs): Promise<void> {
await bucket.delete(path.posix.join(prefix, filename))
}
// adapter.ts
export function createR2Adapter({ bucket, clientUploads }: CreateR2AdapterArgs): Adapter {
return ({ collection, prefix = '' }): GeneratedAdapter => ({
name: 'r2',
clientUploads,
handleDelete: ({ doc: { prefix: docPrefix = '' }, filename }) =>
deleteFile({ bucket, filename, prefix: docPrefix }),
handleUpload: ({ data, file }) =>
uploadFile({
bucket,
buffer: file.buffer,
filename: file.filename,
mimeType: file.mimeType,
prefix: data.prefix || prefix,
}),
staticHandler: (
req,
{ headers, params: { clientUploadContext, filename, prefix: prefixQueryParam } },
) =>
getFile({
bucket,
clientUploadContext,
collection,
filename,
incomingHeaders: headers,
prefix,
prefixQueryParam,
req,
}),
})
}
```
**Bug fix (storage-r2):**
The prefix query param feature (payloadcms#15844) introduced inconsistent logic in
R2:
- `handleUpload`: uses `data.prefix || prefix` (OR logic)
- `staticHandler`: was using `prefix + filePrefix` (AND logic)
Fixed to use consistent OR logic: `filePrefix || prefix`
## Packages affected
- `@payloadcms/storage-r2`
- `@payloadcms/storage-s3`
- `@payloadcms/storage-gcs`
- `@payloadcms/storage-azure`
- `@payloadcms/storage-vercel-blob`
- `@payloadcms/storage-uploadthing`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add optional
?prefix=query string support to the existing/api/:collection/file/:filenameendpoint to disambiguate files that share the same filename but have different storage prefixes.When a storage prefix (e.g., UUID or folder path) is used to make S3 keys unique, multiple documents can share the same
filename. The current endpoint resolves by filename alone, which can match the wrong document in both access control and file retrieval.checkFileAccessaccepts optionalprefixand adds it to thewhereclause alongside existing access filtersgetFilehandler readsprefixfromreq.searchParamsand threads it tocheckFileAccessand handlerparamsgetFilePrefixacceptsexplicitPrefixto skip the DB query when the prefix is already known from the URLstaticHandlerforwardsprefixfrom params togetFilePrefixafterReadhook appends?prefix=to Payload-proxied URLscheckFileAccessandgetFilePrefix, and integration tests for the endpoint'