fix: correctly detect glb & gltf mimetypes during upload (#12623)
### What? The browser was incorrectly setting the mimetype for `.glb` and `.gltf` files to `application/octet-stream` when uploading when they should be receiving proper types consistent with `glb` and `gltf`. This patch adds logic to infer the correct `MIME` type for `.glb` files (`model/gltf-binary`) & `gltf` files (`model/gltf+json`) based on file extension during multipart processing, ensuring consistent MIME type detection regardless of browser behavior. Fixes #12620
This commit is contained in:
@@ -66,6 +66,11 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
||||
const { encoding, filename: name, mimeType: mime } = info
|
||||
const filename = parseFileName(options, name)
|
||||
|
||||
const inferredMimeType =
|
||||
(filename && filename.endsWith('.glb') && 'model/gltf-binary') ||
|
||||
(filename && filename.endsWith('.gltf') && 'model/gltf+json') ||
|
||||
mime
|
||||
|
||||
// Define methods and handlers for upload process.
|
||||
const { cleanup, complete, dataHandler, getFilePath, getFileSize, getHash, getWritePromise } =
|
||||
options.useTempFiles
|
||||
@@ -137,7 +142,7 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
||||
buffer: complete(),
|
||||
encoding,
|
||||
hash: getHash(),
|
||||
mimetype: mime,
|
||||
mimetype: inferredMimeType,
|
||||
size,
|
||||
tempFilePath: getFilePath(),
|
||||
truncated: Boolean('truncated' in file && file.truncated) || false,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { CollectionSlug } from 'payload'
|
||||
/* eslint-disable no-restricted-exports */
|
||||
|
||||
import type { CollectionSlug, File } from 'payload'
|
||||
|
||||
import path from 'path'
|
||||
import { getFileByPath } from 'payload'
|
||||
@@ -28,6 +30,7 @@ import {
|
||||
reduceSlug,
|
||||
relationPreviewSlug,
|
||||
relationSlug,
|
||||
threeDimensionalSlug,
|
||||
unstoredMediaSlug,
|
||||
versionSlug,
|
||||
withoutEnlargeSlug,
|
||||
@@ -797,6 +800,14 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: threeDimensionalSlug,
|
||||
fields: [],
|
||||
upload: {
|
||||
crop: false,
|
||||
focalPoint: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
onInit: async (payload) => {
|
||||
const uploadsDir = path.resolve(dirname, './media')
|
||||
@@ -893,39 +904,39 @@ export default buildConfigWithDefaults({
|
||||
|
||||
// Create admin thumbnail media
|
||||
await payload.create({
|
||||
collection: AdminThumbnailSize.slug,
|
||||
collection: AdminThumbnailSize.slug as CollectionSlug,
|
||||
data: {},
|
||||
file: {
|
||||
...audioFile,
|
||||
name: 'audio-thumbnail.mp3', // Override to avoid conflicts
|
||||
},
|
||||
} as File,
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: AdminThumbnailSize.slug,
|
||||
collection: AdminThumbnailSize.slug as CollectionSlug,
|
||||
data: {},
|
||||
file: {
|
||||
...imageFile,
|
||||
name: `thumb-${imageFile.name}`,
|
||||
},
|
||||
name: `thumb-${imageFile?.name}`,
|
||||
} as File,
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: AdminThumbnailFunction.slug,
|
||||
collection: AdminThumbnailFunction.slug as CollectionSlug,
|
||||
data: {},
|
||||
file: {
|
||||
...imageFile,
|
||||
name: `function-image-${imageFile.name}`,
|
||||
},
|
||||
name: `function-image-${imageFile?.name}`,
|
||||
} as File,
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: AdminThumbnailWithSearchQueries.slug,
|
||||
collection: AdminThumbnailWithSearchQueries.slug as CollectionSlug,
|
||||
data: {},
|
||||
file: {
|
||||
...imageFile,
|
||||
name: `searchQueries-image-${imageFile.name}`,
|
||||
},
|
||||
name: `searchQueries-image-${imageFile?.name}`,
|
||||
} as File,
|
||||
})
|
||||
|
||||
// Create media with and without relation preview
|
||||
@@ -940,8 +951,8 @@ export default buildConfigWithDefaults({
|
||||
data: {},
|
||||
file: {
|
||||
...imageFile,
|
||||
name: `withoutCacheTags-image-${imageFile.name}`,
|
||||
},
|
||||
name: `withoutCacheTags-image-${imageFile?.name}`,
|
||||
} as File,
|
||||
})
|
||||
|
||||
const { id: uploadedImageWithoutPreview } = await payload.create({
|
||||
|
||||
BIN
test/uploads/duck.glb
Normal file
BIN
test/uploads/duck.glb
Normal file
Binary file not shown.
@@ -36,6 +36,7 @@ import {
|
||||
mediaWithoutCacheTagsSlug,
|
||||
relationPreviewSlug,
|
||||
relationSlug,
|
||||
threeDimensionalSlug,
|
||||
withMetadataSlug,
|
||||
withOnlyJPEGMetadataSlug,
|
||||
withoutEnlargeSlug,
|
||||
@@ -71,6 +72,7 @@ let customUploadFieldURL: AdminUrlUtil
|
||||
let hideFileInputOnCreateURL: AdminUrlUtil
|
||||
let bestFitURL: AdminUrlUtil
|
||||
let withoutEnlargementResizeOptionsURL: AdminUrlUtil
|
||||
let threeDimensionalURL: AdminUrlUtil
|
||||
let consoleErrorsFromPage: string[] = []
|
||||
let collectErrorsFromPage: () => boolean
|
||||
let stopCollectingErrorsFromPage: () => boolean
|
||||
@@ -107,6 +109,7 @@ describe('Uploads', () => {
|
||||
hideFileInputOnCreateURL = new AdminUrlUtil(serverURL, hideFileInputOnCreateSlug)
|
||||
bestFitURL = new AdminUrlUtil(serverURL, 'best-fit')
|
||||
withoutEnlargementResizeOptionsURL = new AdminUrlUtil(serverURL, withoutEnlargeSlug)
|
||||
threeDimensionalURL = new AdminUrlUtil(serverURL, threeDimensionalSlug)
|
||||
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
@@ -176,7 +179,7 @@ describe('Uploads', () => {
|
||||
})
|
||||
).docs[0]
|
||||
|
||||
await page.goto(relationURL.edit(relationDoc.id))
|
||||
await page.goto(relationURL.edit(relationDoc!.id))
|
||||
|
||||
const filename = page.locator('.upload-relationship-details__filename a').nth(0)
|
||||
await expect(filename).toContainText('image.png')
|
||||
@@ -245,6 +248,19 @@ describe('Uploads', () => {
|
||||
await expect(fileMetaSizeType).toHaveText(/image\/png/)
|
||||
})
|
||||
|
||||
test('should show proper mimetype for glb file', async () => {
|
||||
await page.goto(threeDimensionalURL.create)
|
||||
|
||||
await page.setInputFiles('input[type="file"]', path.resolve(dirname, './duck.glb'))
|
||||
const filename = page.locator('.file-field__filename')
|
||||
await expect(filename).toHaveValue('duck.glb')
|
||||
|
||||
await saveDocAndAssert(page)
|
||||
|
||||
const fileMetaSizeType = page.locator('.file-meta__size-type')
|
||||
await expect(fileMetaSizeType).toHaveText(/model\/gltf-binary/)
|
||||
})
|
||||
|
||||
test('should create animated file upload', async () => {
|
||||
await page.goto(animatedTypeMediaURL.create)
|
||||
|
||||
@@ -298,7 +314,7 @@ describe('Uploads', () => {
|
||||
})
|
||||
).docs[0]
|
||||
|
||||
await page.goto(mediaURL.edit(pngDoc.id))
|
||||
await page.goto(mediaURL.edit(pngDoc!.id))
|
||||
|
||||
await page.locator('.file-field__previewSizes').click()
|
||||
|
||||
@@ -432,7 +448,7 @@ describe('Uploads', () => {
|
||||
})
|
||||
).docs[0]
|
||||
|
||||
await page.goto(audioURL.edit(audioDoc.id))
|
||||
await page.goto(audioURL.edit(audioDoc!.id))
|
||||
|
||||
// remove the selection and open the list drawer
|
||||
await wait(500) // flake workaround
|
||||
@@ -478,7 +494,7 @@ describe('Uploads', () => {
|
||||
})
|
||||
).docs[0]
|
||||
|
||||
await page.goto(audioURL.edit(audioDoc.id))
|
||||
await page.goto(audioURL.edit(audioDoc!.id))
|
||||
|
||||
// remove the selection and open the list drawer
|
||||
await wait(500) // flake workaround
|
||||
@@ -539,7 +555,7 @@ describe('Uploads', () => {
|
||||
})
|
||||
).docs[0]
|
||||
|
||||
await page.goto(mediaWithoutCacheTagsSlugURL.edit(imageDoc.id))
|
||||
await page.goto(mediaWithoutCacheTagsSlugURL.edit(imageDoc!.id))
|
||||
|
||||
const genericUploadImage = page.locator('.file-details .thumbnail img')
|
||||
|
||||
@@ -569,7 +585,7 @@ describe('Uploads', () => {
|
||||
})
|
||||
).docs[0]
|
||||
|
||||
await page.goto(adminThumbnailFunctionURL.edit(imageDoc.id))
|
||||
await page.goto(adminThumbnailFunctionURL.edit(imageDoc!.id))
|
||||
|
||||
const genericUploadImage = page.locator('.file-details .thumbnail img')
|
||||
|
||||
@@ -605,7 +621,7 @@ describe('Uploads', () => {
|
||||
const imageID = page.url().split('/').pop()
|
||||
|
||||
const { doc: uploadedImage } = await client.findByID({
|
||||
id: imageID,
|
||||
id: imageID as number | string,
|
||||
slug: mediaSlug,
|
||||
auth: true,
|
||||
})
|
||||
@@ -630,7 +646,7 @@ describe('Uploads', () => {
|
||||
const mediaID = page.url().split('/').pop()
|
||||
|
||||
const { doc: mediaDoc } = await client.findByID({
|
||||
id: mediaID,
|
||||
id: mediaID as number | string,
|
||||
slug: withMetadataSlug,
|
||||
auth: true,
|
||||
})
|
||||
@@ -657,7 +673,7 @@ describe('Uploads', () => {
|
||||
const mediaID = page.url().split('/').pop()
|
||||
|
||||
const { doc: mediaDoc } = await client.findByID({
|
||||
id: mediaID,
|
||||
id: mediaID as number | string,
|
||||
slug: withoutMetadataSlug,
|
||||
auth: true,
|
||||
})
|
||||
@@ -684,7 +700,7 @@ describe('Uploads', () => {
|
||||
const jpegMediaID = page.url().split('/').pop()
|
||||
|
||||
const { doc: jpegMediaDoc } = await client.findByID({
|
||||
id: jpegMediaID,
|
||||
id: jpegMediaID as number | string,
|
||||
slug: withOnlyJPEGMetadataSlug,
|
||||
auth: true,
|
||||
})
|
||||
@@ -710,7 +726,7 @@ describe('Uploads', () => {
|
||||
const webpMediaID = page.url().split('/').pop()
|
||||
|
||||
const { doc: webpMediaDoc } = await client.findByID({
|
||||
id: webpMediaID,
|
||||
id: webpMediaID as number | string,
|
||||
slug: withOnlyJPEGMetadataSlug,
|
||||
auth: true,
|
||||
})
|
||||
@@ -1210,13 +1226,13 @@ describe('Uploads', () => {
|
||||
const redSquareMediaID = page.url().split('/').pop() // get the ID of the doc
|
||||
|
||||
const { doc: greenDoc } = await client.findByID({
|
||||
id: greenSquareMediaID,
|
||||
id: greenSquareMediaID as number | string,
|
||||
slug: mediaSlug,
|
||||
auth: true,
|
||||
})
|
||||
|
||||
const { doc: redDoc } = await client.findByID({
|
||||
id: redSquareMediaID,
|
||||
id: redSquareMediaID as number | string,
|
||||
slug: mediaSlug,
|
||||
auth: true,
|
||||
})
|
||||
@@ -1254,7 +1270,7 @@ describe('Uploads', () => {
|
||||
const redSquareMediaID = page.url().split('/').pop() // get the ID of the doc
|
||||
|
||||
const { doc: redDoc } = await client.findByID({
|
||||
id: redSquareMediaID,
|
||||
id: redSquareMediaID as number | string,
|
||||
slug: focalOnlySlug,
|
||||
auth: true,
|
||||
})
|
||||
|
||||
@@ -82,6 +82,7 @@ export interface Config {
|
||||
media: Media;
|
||||
'animated-type-media': AnimatedTypeMedia;
|
||||
enlarge: Enlarge;
|
||||
'without-enlarge': WithoutEnlarge;
|
||||
reduce: Reduce;
|
||||
'media-trim': MediaTrim;
|
||||
'custom-file-name-media': CustomFileNameMedia;
|
||||
@@ -103,6 +104,7 @@ export interface Config {
|
||||
'hide-file-input-on-create': HideFileInputOnCreate;
|
||||
'best-fit': BestFit;
|
||||
'list-view-preview': ListViewPreview;
|
||||
'three-dimensional': ThreeDimensional;
|
||||
users: User;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
@@ -125,6 +127,7 @@ export interface Config {
|
||||
media: MediaSelect<false> | MediaSelect<true>;
|
||||
'animated-type-media': AnimatedTypeMediaSelect<false> | AnimatedTypeMediaSelect<true>;
|
||||
enlarge: EnlargeSelect<false> | EnlargeSelect<true>;
|
||||
'without-enlarge': WithoutEnlargeSelect<false> | WithoutEnlargeSelect<true>;
|
||||
reduce: ReduceSelect<false> | ReduceSelect<true>;
|
||||
'media-trim': MediaTrimSelect<false> | MediaTrimSelect<true>;
|
||||
'custom-file-name-media': CustomFileNameMediaSelect<false> | CustomFileNameMediaSelect<true>;
|
||||
@@ -146,6 +149,7 @@ export interface Config {
|
||||
'hide-file-input-on-create': HideFileInputOnCreateSelect<false> | HideFileInputOnCreateSelect<true>;
|
||||
'best-fit': BestFitSelect<false> | BestFitSelect<true>;
|
||||
'list-view-preview': ListViewPreviewSelect<false> | ListViewPreviewSelect<true>;
|
||||
'three-dimensional': ThreeDimensionalSelect<false> | ThreeDimensionalSelect<true>;
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
@@ -848,6 +852,24 @@ export interface Enlarge {
|
||||
};
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "without-enlarge".
|
||||
*/
|
||||
export interface WithoutEnlarge {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
thumbnailURL?: string | null;
|
||||
filename?: string | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
focalX?: number | null;
|
||||
focalY?: number | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "reduce".
|
||||
@@ -1289,6 +1311,22 @@ export interface ListViewPreview {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "three-dimensional".
|
||||
*/
|
||||
export interface ThreeDimensional {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
thumbnailURL?: string | null;
|
||||
filename?: string | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
@@ -1373,6 +1411,10 @@ export interface PayloadLockedDocument {
|
||||
relationTo: 'enlarge';
|
||||
value: string | Enlarge;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'without-enlarge';
|
||||
value: string | WithoutEnlarge;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'reduce';
|
||||
value: string | Reduce;
|
||||
@@ -1457,6 +1499,10 @@ export interface PayloadLockedDocument {
|
||||
relationTo: 'list-view-preview';
|
||||
value: string | ListViewPreview;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'three-dimensional';
|
||||
value: string | ThreeDimensional;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
@@ -2219,6 +2265,23 @@ export interface EnlargeSelect<T extends boolean = true> {
|
||||
};
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "without-enlarge_select".
|
||||
*/
|
||||
export interface WithoutEnlargeSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
url?: T;
|
||||
thumbnailURL?: T;
|
||||
filename?: T;
|
||||
mimeType?: T;
|
||||
filesize?: T;
|
||||
width?: T;
|
||||
height?: T;
|
||||
focalX?: T;
|
||||
focalY?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "reduce_select".
|
||||
@@ -2692,6 +2755,21 @@ export interface ListViewPreviewSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "three-dimensional_select".
|
||||
*/
|
||||
export interface ThreeDimensionalSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
url?: T;
|
||||
thumbnailURL?: T;
|
||||
filename?: T;
|
||||
mimeType?: T;
|
||||
filesize?: T;
|
||||
width?: T;
|
||||
height?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
|
||||
@@ -25,3 +25,4 @@ export const withOnlyJPEGMetadataSlug = 'with-only-jpeg-meta-data'
|
||||
export const customFileNameMediaSlug = 'custom-file-name-media'
|
||||
|
||||
export const listViewPreviewSlug = 'list-view-preview'
|
||||
export const threeDimensionalSlug = 'three-dimensional'
|
||||
|
||||
Reference in New Issue
Block a user