fix: ensure body limit is respected (#5807)
Co-authored-by: James <james@trbl.design>
This commit is contained in:
@@ -77,7 +77,7 @@ export const tempFileHandler: Handler = (options, fieldname, filename) => {
|
||||
}
|
||||
|
||||
export const memHandler: Handler = (options, fieldname, filename) => {
|
||||
const buffers = []
|
||||
const buffers: Buffer[] = []
|
||||
const hash = crypto.createHash('md5')
|
||||
let fileSize = 0
|
||||
let completed = false
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const ACCEPTABLE_CONTENT_TYPE = /^multipart\/['"()+-_]+(?:; ?['"()+-_]*)+$/i
|
||||
const ACCEPTABLE_CONTENT_TYPE = /multipart\/['"()+-_]+(?:; ?['"()+-_]*)+$/i
|
||||
const UNACCEPTABLE_METHODS = new Set(['GET', 'HEAD', 'DELETE', 'OPTIONS', 'CONNECT', 'TRACE'])
|
||||
|
||||
const hasBody = (req: Request): boolean => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Busboy from 'busboy'
|
||||
import httpStatus from 'http-status'
|
||||
import { APIError } from 'payload/errors'
|
||||
|
||||
import type { NextFileUploadOptions, NextFileUploadResponse } from './index.js'
|
||||
@@ -17,6 +18,17 @@ type ProcessMultipart = (args: {
|
||||
}) => Promise<NextFileUploadResponse>
|
||||
export const processMultipart: ProcessMultipart = async ({ options, request }) => {
|
||||
let parsingRequest = true
|
||||
|
||||
let fileCount = 0
|
||||
let filesCompleted = 0
|
||||
let allFilesHaveResolved: (value?: unknown) => void
|
||||
let failedResolvingFiles: (err: Error) => void
|
||||
|
||||
const allFilesComplete = new Promise((res, rej) => {
|
||||
allFilesHaveResolved = res
|
||||
failedResolvingFiles = rej
|
||||
})
|
||||
|
||||
const result: NextFileUploadResponse = {
|
||||
fields: undefined,
|
||||
files: undefined,
|
||||
@@ -36,6 +48,7 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
||||
|
||||
// Build req.files fields
|
||||
busboy.on('file', (field, file, info) => {
|
||||
fileCount += 1
|
||||
// Parse file name(cutting huge names, decoding, etc..).
|
||||
const { encoding, filename: name, mimeType: mime } = info
|
||||
const filename = parseFileName(options, name)
|
||||
@@ -73,7 +86,9 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
||||
debugLog(options, `Aborting upload because of size limit ${field}->${filename}.`)
|
||||
cleanup()
|
||||
parsingRequest = false
|
||||
throw new APIError(options.responseOnLimit, 413, { size: getFileSize() })
|
||||
throw new APIError(options.responseOnLimit, httpStatus.REQUEST_ENTITY_TOO_LARGE, {
|
||||
size: getFileSize(),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -95,6 +110,8 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
||||
return debugLog(options, `Don't add file instance if original name and size are empty`)
|
||||
}
|
||||
|
||||
filesCompleted += 1
|
||||
|
||||
result.files = buildFields(
|
||||
result.files,
|
||||
field,
|
||||
@@ -117,19 +134,25 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
||||
request[waitFlushProperty] = []
|
||||
}
|
||||
request[waitFlushProperty].push(writePromise)
|
||||
|
||||
if (filesCompleted === fileCount) {
|
||||
allFilesHaveResolved()
|
||||
}
|
||||
})
|
||||
|
||||
file.on('error', (err) => {
|
||||
uploadTimer.clear()
|
||||
debugLog(options, `File Error: ${err.message}`)
|
||||
cleanup()
|
||||
failedResolvingFiles(err)
|
||||
})
|
||||
|
||||
// Start upload process.
|
||||
debugLog(options, `New upload started ${field}->${filename}, bytes:${getFileSize()}`)
|
||||
uploadTimer.set()
|
||||
})
|
||||
|
||||
busboy.on('finish', () => {
|
||||
busboy.on('finish', async () => {
|
||||
debugLog(options, `Busboy finished parsing request.`)
|
||||
if (options.parseNested) {
|
||||
result.fields = processNested(result.fields)
|
||||
@@ -137,20 +160,27 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
||||
}
|
||||
|
||||
if (request[waitFlushProperty]) {
|
||||
Promise.all(request[waitFlushProperty]).then(() => {
|
||||
delete request[waitFlushProperty]
|
||||
})
|
||||
try {
|
||||
await Promise.all(request[waitFlushProperty]).then(() => {
|
||||
delete request[waitFlushProperty]
|
||||
})
|
||||
} catch (err) {
|
||||
debugLog(options, `Error waiting for file write promises: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
busboy.on('error', (err) => {
|
||||
debugLog(options, `Busboy error`)
|
||||
parsingRequest = false
|
||||
throw new APIError('Busboy error parsing multipart request', 500)
|
||||
throw new APIError('Busboy error parsing multipart request', httpStatus.BAD_REQUEST)
|
||||
})
|
||||
|
||||
const reader = request.body.getReader()
|
||||
|
||||
// Start parsing request
|
||||
while (parsingRequest) {
|
||||
const { done, value } = await reader.read()
|
||||
|
||||
@@ -163,5 +193,7 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
||||
}
|
||||
}
|
||||
|
||||
if (fileCount !== 0) await allFilesComplete
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ let tempCounter = 0
|
||||
export const debugLog = (options: NextFileUploadOptions, msg: string) => {
|
||||
const opts = options || {}
|
||||
if (!opts.debug) return false
|
||||
console.log(`Express-file-upload: ${msg}`) // eslint-disable-line
|
||||
console.log(`Next-file-upload: ${msg}`) // eslint-disable-line
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -287,8 +287,9 @@ export const parseFileName: ParseFileName = (opts, fileName) => {
|
||||
? opts.safeFileNames
|
||||
: SAFE_FILE_NAME_REGEX
|
||||
// Parse file name extension.
|
||||
let { name, extension } = parseFileNameExtension(opts.preserveExtension, parsedName)
|
||||
if (extension.length) extension = '.' + extension.replace(nameRegex, '')
|
||||
const parsedFileName = parseFileNameExtension(opts.preserveExtension, parsedName)
|
||||
if (parsedFileName.extension.length)
|
||||
parsedFileName.extension = '.' + parsedFileName.extension.replace(nameRegex, '')
|
||||
|
||||
return name.replace(nameRegex, '').concat(extension)
|
||||
return parsedFileName.name.replace(nameRegex, '').concat(parsedFileName.extension)
|
||||
}
|
||||
|
||||
@@ -35,8 +35,9 @@ export const preview: CollectionRouteHandlerWithID = async ({ id, collection, re
|
||||
token,
|
||||
})
|
||||
} catch (err) {
|
||||
routeError({
|
||||
return routeError({
|
||||
collection,
|
||||
config: req.payload.config,
|
||||
err,
|
||||
req,
|
||||
})
|
||||
|
||||
@@ -66,6 +66,7 @@ export const getFile = async ({ collection, filename, req }: Args): Promise<Resp
|
||||
} catch (error) {
|
||||
return routeError({
|
||||
collection,
|
||||
config: req.payload.config,
|
||||
err: error,
|
||||
req,
|
||||
})
|
||||
|
||||
@@ -35,7 +35,8 @@ export const preview: GlobalRouteHandler = async ({ globalConfig, req }) => {
|
||||
token,
|
||||
})
|
||||
} catch (err) {
|
||||
routeError({
|
||||
return routeError({
|
||||
config: req.payload.config,
|
||||
err,
|
||||
req,
|
||||
})
|
||||
|
||||
@@ -303,6 +303,7 @@ export const GET =
|
||||
} catch (error) {
|
||||
return routeError({
|
||||
collection,
|
||||
config,
|
||||
err: error,
|
||||
req,
|
||||
})
|
||||
@@ -445,6 +446,7 @@ export const POST =
|
||||
} catch (error) {
|
||||
return routeError({
|
||||
collection,
|
||||
config,
|
||||
err: error,
|
||||
req,
|
||||
})
|
||||
@@ -514,6 +516,7 @@ export const DELETE =
|
||||
} catch (error) {
|
||||
return routeError({
|
||||
collection,
|
||||
config,
|
||||
err: error,
|
||||
req,
|
||||
})
|
||||
@@ -583,6 +586,7 @@ export const PATCH =
|
||||
} catch (error) {
|
||||
return routeError({
|
||||
collection,
|
||||
config,
|
||||
err: error,
|
||||
req,
|
||||
})
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { Collection, PayloadRequest } from 'payload/types'
|
||||
import type { Collection, PayloadRequest, SanitizedConfig } from 'payload/types'
|
||||
|
||||
import httpStatus from 'http-status'
|
||||
import { APIError } from 'payload/errors'
|
||||
|
||||
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
|
||||
|
||||
export type ErrorResponse = { data?: any; errors: unknown[]; stack?: string }
|
||||
|
||||
const formatErrors = (incoming: { [key: string]: unknown } | APIError): ErrorResponse => {
|
||||
@@ -66,26 +68,33 @@ const formatErrors = (incoming: { [key: string]: unknown } | APIError): ErrorRes
|
||||
}
|
||||
}
|
||||
|
||||
export const routeError = ({
|
||||
export const routeError = async ({
|
||||
collection,
|
||||
config: configArg,
|
||||
err,
|
||||
req,
|
||||
}: {
|
||||
collection?: Collection
|
||||
config: Promise<SanitizedConfig> | SanitizedConfig
|
||||
err: APIError
|
||||
req: PayloadRequest
|
||||
}) => {
|
||||
if (!req?.payload) {
|
||||
return Response.json(
|
||||
{
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
},
|
||||
{ status: httpStatus.INTERNAL_SERVER_ERROR },
|
||||
)
|
||||
let payload = req?.payload
|
||||
|
||||
if (!payload) {
|
||||
try {
|
||||
payload = await getPayloadHMR({ config: configArg })
|
||||
} catch (e) {
|
||||
return Response.json(
|
||||
{
|
||||
message: 'There was an error initializing Payload',
|
||||
},
|
||||
{ status: httpStatus.INTERNAL_SERVER_ERROR },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const { config, logger } = req.payload
|
||||
const { config, logger } = payload
|
||||
|
||||
let response = formatErrors(err)
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Collection, CustomPayloadRequest, SanitizedConfig } from 'payload/types'
|
||||
|
||||
import type { NextFileUploadOptions } from '../next-fileupload/index.js'
|
||||
|
||||
import { nextFileUpload } from '../next-fileupload/index.js'
|
||||
|
||||
type GetDataAndFile = (args: {
|
||||
@@ -10,68 +12,53 @@ type GetDataAndFile = (args: {
|
||||
data: Record<string, any>
|
||||
file: CustomPayloadRequest['file']
|
||||
}>
|
||||
export const getDataAndFile: GetDataAndFile = async ({ collection, config, request }) => {
|
||||
export const getDataAndFile: GetDataAndFile = async ({
|
||||
collection,
|
||||
config,
|
||||
request: incomingRequest,
|
||||
}) => {
|
||||
let data: Record<string, any> = undefined
|
||||
let file: CustomPayloadRequest['file'] = undefined
|
||||
|
||||
if (['PATCH', 'POST', 'PUT'].includes(request.method.toUpperCase()) && request.body) {
|
||||
if (
|
||||
['PATCH', 'POST', 'PUT'].includes(incomingRequest.method.toUpperCase()) &&
|
||||
incomingRequest.body
|
||||
) {
|
||||
const request = new Request(incomingRequest)
|
||||
const [contentType] = (request.headers.get('Content-Type') || '').split(';')
|
||||
|
||||
if (contentType === 'application/json') {
|
||||
try {
|
||||
data = await request.json()
|
||||
} catch (error) {
|
||||
data = {}
|
||||
}
|
||||
} else if (contentType === 'multipart/form-data') {
|
||||
// possible upload request
|
||||
if (collection?.config?.upload) {
|
||||
// load file in memory
|
||||
if (!config.upload?.useTempFiles) {
|
||||
const formData = await request.formData()
|
||||
const formFile = formData.get('file')
|
||||
|
||||
if (formFile instanceof Blob) {
|
||||
const bytes = await formFile.arrayBuffer()
|
||||
const buffer = Buffer.from(bytes)
|
||||
|
||||
file = {
|
||||
name: formFile.name,
|
||||
data: buffer,
|
||||
mimetype: formFile.type,
|
||||
size: formFile.size,
|
||||
}
|
||||
}
|
||||
|
||||
const payloadData = formData.get('_payload')
|
||||
|
||||
if (typeof payloadData === 'string') {
|
||||
data = JSON.parse(payloadData)
|
||||
}
|
||||
} else {
|
||||
// store temp file on disk
|
||||
const { error, fields, files } = await nextFileUpload({
|
||||
options: config.upload as any,
|
||||
request,
|
||||
})
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message)
|
||||
}
|
||||
|
||||
if (files?.file) file = files.file
|
||||
|
||||
if (fields?._payload && typeof fields._payload === 'string') {
|
||||
data = JSON.parse(fields._payload)
|
||||
}
|
||||
const bodyByteSize = parseInt(request.headers.get('Content-Length') || '0', 10)
|
||||
const upperByteLimit =
|
||||
typeof config.upload?.limits?.fieldSize === 'number'
|
||||
? config.upload.limits.fields
|
||||
: undefined
|
||||
if (bodyByteSize <= upperByteLimit || upperByteLimit === undefined) {
|
||||
try {
|
||||
data = await request.json()
|
||||
} catch (error) {
|
||||
data = {}
|
||||
}
|
||||
} else {
|
||||
// non upload request
|
||||
const formData = await request.formData()
|
||||
const payloadData = formData.get('_payload')
|
||||
throw new Error('Request body size exceeds the limit')
|
||||
}
|
||||
} else {
|
||||
if (request.headers.has('Content-Length') && request.headers.get('Content-Length') !== '0') {
|
||||
const { error, fields, files } = await nextFileUpload({
|
||||
options: config.upload as NextFileUploadOptions,
|
||||
request,
|
||||
})
|
||||
|
||||
if (typeof payloadData === 'string') {
|
||||
data = JSON.parse(payloadData)
|
||||
if (error) {
|
||||
throw new Error(error.message)
|
||||
}
|
||||
|
||||
if (collection?.config?.upload && files?.file) {
|
||||
file = files.file
|
||||
}
|
||||
|
||||
if (fields?._payload && typeof fields._payload === 'string') {
|
||||
data = JSON.parse(fields._payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,21 +2,20 @@ import type { SanitizedConfig } from 'payload/config'
|
||||
import type { Where } from 'payload/types'
|
||||
import type { ParsedQs } from 'qs'
|
||||
|
||||
import {
|
||||
REST_DELETE as createDELETE,
|
||||
REST_GET as createGET,
|
||||
REST_PATCH as createPATCH,
|
||||
REST_POST as createPOST,
|
||||
} from '@payloadcms/next/routes'
|
||||
import { GRAPHQL_POST as createGraphqlPOST } from '@payloadcms/next/routes'
|
||||
import QueryString from 'qs'
|
||||
|
||||
import { GRAPHQL_POST as createGraphqlPOST } from '../../packages/next/src/routes/graphql/index.js'
|
||||
import {
|
||||
DELETE as createDELETE,
|
||||
GET as createGET,
|
||||
PATCH as createPATCH,
|
||||
POST as createPOST,
|
||||
} from '../../packages/next/src/routes/rest/index.js'
|
||||
import { devUser } from '../credentials.js'
|
||||
|
||||
type ValidPath = `/${string}`
|
||||
type RequestOptions = {
|
||||
auth?: boolean
|
||||
file?: boolean
|
||||
query?: {
|
||||
depth?: number
|
||||
fallbackLocale?: string
|
||||
@@ -28,6 +27,10 @@ type RequestOptions = {
|
||||
}
|
||||
}
|
||||
|
||||
type FileArg = {
|
||||
file?: Omit<File, 'webkitRelativePath'>
|
||||
}
|
||||
|
||||
function generateQueryString(query: RequestOptions['query'], params: ParsedQs): string {
|
||||
return QueryString.stringify(
|
||||
{
|
||||
@@ -67,12 +70,16 @@ export class NextRESTClient {
|
||||
this._GRAPHQL_POST = createGraphqlPOST(config)
|
||||
}
|
||||
|
||||
private buildHeaders(options: RequestInit & RequestOptions): Headers {
|
||||
private buildHeaders(options: RequestInit & RequestOptions & FileArg): Headers {
|
||||
const defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
const headers = new Headers({
|
||||
...(options?.file ? {} : defaultHeaders),
|
||||
...(options?.file
|
||||
? {
|
||||
'Content-Length': options.file.size.toString(),
|
||||
}
|
||||
: defaultHeaders),
|
||||
...(options?.headers || {}),
|
||||
})
|
||||
|
||||
@@ -141,7 +148,7 @@ export class NextRESTClient {
|
||||
return this._GRAPHQL_POST(request)
|
||||
}
|
||||
|
||||
async PATCH(path: ValidPath, options: RequestInit & RequestOptions): Promise<Response> {
|
||||
async PATCH(path: ValidPath, options: RequestInit & RequestOptions & FileArg): Promise<Response> {
|
||||
const { url, slug, params } = this.generateRequestParts(path)
|
||||
const { query, ...rest } = options
|
||||
const queryParams = generateQueryString(query, params)
|
||||
@@ -156,11 +163,10 @@ export class NextRESTClient {
|
||||
|
||||
async POST(
|
||||
path: ValidPath,
|
||||
options: RequestInit & RequestOptions & { file?: boolean } = {},
|
||||
options: RequestInit & RequestOptions & FileArg = {},
|
||||
): Promise<Response> {
|
||||
const { url, slug, params } = this.generateRequestParts(path)
|
||||
const queryParams = generateQueryString({}, params)
|
||||
|
||||
const request = new Request(`${url}${queryParams}`, {
|
||||
...options,
|
||||
method: 'POST',
|
||||
|
||||
BIN
test/uploads/2mb.jpg
Normal file
BIN
test/uploads/2mb.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
@@ -452,6 +452,13 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
},
|
||||
],
|
||||
upload: {
|
||||
// debug: true,
|
||||
abortOnLimit: true,
|
||||
limits: {
|
||||
fileSize: 2_000_000, // 2MB
|
||||
},
|
||||
},
|
||||
onInit: async (payload) => {
|
||||
const uploadsDir = path.resolve(dirname, './media')
|
||||
removeFiles(path.normalize(uploadsDir))
|
||||
|
||||
37
test/uploads/createStreamableFile.ts
Normal file
37
test/uploads/createStreamableFile.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { File } from 'buffer'
|
||||
import NodeFormData from 'form-data'
|
||||
import fs from 'fs'
|
||||
import { open } from 'node:fs/promises'
|
||||
import { basename } from 'node:path'
|
||||
|
||||
import { getMimeType } from './getMimeType.js'
|
||||
|
||||
export async function createStreamableFile(
|
||||
path: string,
|
||||
): Promise<{ file: File; handle: fs.promises.FileHandle }> {
|
||||
const name = basename(path)
|
||||
const handle = await open(path)
|
||||
|
||||
const { type } = getMimeType(path)
|
||||
|
||||
const file = new File([], name, { type })
|
||||
file.stream = () => handle.readableWebStream()
|
||||
|
||||
const formDataNode = new NodeFormData()
|
||||
formDataNode.append('file', fs.createReadStream(path))
|
||||
|
||||
const contentLength = await new Promise((resolve, reject) => {
|
||||
formDataNode.getLength((err, length) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(length)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Set correct size otherwise, fetch will encounter UND_ERR_REQ_CONTENT_LENGTH_MISMATCH
|
||||
Object.defineProperty(file, 'size', { get: () => contentLength })
|
||||
|
||||
return { file, handle }
|
||||
}
|
||||
@@ -241,6 +241,17 @@ describe('uploads', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('should throw error when file is larger than the limit and abortOnLimit is true', async () => {
|
||||
await page.goto(mediaURL.create)
|
||||
await page.setInputFiles('input[type="file"]', path.resolve(dirname, './2mb.jpg'))
|
||||
|
||||
await wait(500) // TODO: Fix this
|
||||
await page.click('#action-save', { delay: 100 })
|
||||
await expect(page.locator('.Toastify .Toastify__toast--error')).toContainText(
|
||||
'File size limit has been reached',
|
||||
)
|
||||
})
|
||||
|
||||
test('Should render adminThumbnail when using a function', async () => {
|
||||
await page.goto(adminThumbnailFunctionURL.list)
|
||||
await page.waitForURL(adminThumbnailFunctionURL.list)
|
||||
|
||||
32
test/uploads/getMimeType.ts
Normal file
32
test/uploads/getMimeType.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import path from 'path'
|
||||
|
||||
export const getMimeType = (
|
||||
filePath: string,
|
||||
): {
|
||||
filename: string
|
||||
type: string
|
||||
} => {
|
||||
const ext = path.extname(filePath).slice(1)
|
||||
let type: string
|
||||
switch (ext) {
|
||||
case 'png':
|
||||
type = 'image/png'
|
||||
break
|
||||
case 'jpg':
|
||||
type = 'image/jpeg'
|
||||
break
|
||||
case 'jpeg':
|
||||
type = 'image/jpeg'
|
||||
break
|
||||
case 'svg':
|
||||
type = 'image/svg+xml'
|
||||
break
|
||||
default:
|
||||
type = 'image/png'
|
||||
}
|
||||
|
||||
return {
|
||||
filename: path.basename(filePath),
|
||||
type,
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import { File as FileBuffer } from 'buffer'
|
||||
import NodeFormData from 'form-data'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { getFileByPath } from 'payload/uploads'
|
||||
@@ -13,6 +11,7 @@ import type { Enlarge, Media } from './payload-types.js'
|
||||
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
import configPromise from './config.js'
|
||||
import { createStreamableFile } from './createStreamableFile.js'
|
||||
import {
|
||||
enlargeSlug,
|
||||
mediaSlug,
|
||||
@@ -24,54 +23,6 @@ import {
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
const getMimeType = (
|
||||
filePath: string,
|
||||
): {
|
||||
filename: string
|
||||
type: string
|
||||
} => {
|
||||
const ext = path.extname(filePath).slice(1)
|
||||
let type: string
|
||||
switch (ext) {
|
||||
case 'png':
|
||||
type = 'image/png'
|
||||
break
|
||||
case 'jpg':
|
||||
type = 'image/jpeg'
|
||||
break
|
||||
case 'jpeg':
|
||||
type = 'image/jpeg'
|
||||
break
|
||||
case 'svg':
|
||||
type = 'image/svg+xml'
|
||||
break
|
||||
default:
|
||||
type = 'image/png'
|
||||
}
|
||||
|
||||
return {
|
||||
filename: path.basename(filePath),
|
||||
type,
|
||||
}
|
||||
}
|
||||
const bufferToFileBlob = async (filePath: string): Promise<File> =>
|
||||
new Promise((resolve, reject) => {
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if (err) {
|
||||
console.error(`Error reading file at ${filePath}:`, err)
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
|
||||
const { filename, type } = getMimeType(filePath)
|
||||
|
||||
// Convert type FileBuffer > unknown > File
|
||||
// The File type expects webkitRelativePath, we don't have that
|
||||
resolve(new FileBuffer([data], filename, { type }) as unknown as File)
|
||||
})
|
||||
})
|
||||
|
||||
const stat = promisify(fs.stat)
|
||||
|
||||
let restClient: NextRESTClient
|
||||
@@ -90,20 +41,22 @@ describe('Collections - Uploads', () => {
|
||||
}
|
||||
})
|
||||
|
||||
describe('REST', () => {
|
||||
describe('REST API', () => {
|
||||
describe('create', () => {
|
||||
it('creates from form data given a png', async () => {
|
||||
const formData = new FormData()
|
||||
const filePath = path.join(dirname, './image.png')
|
||||
|
||||
formData.append('file', await bufferToFileBlob(filePath))
|
||||
const { file, handle } = await createStreamableFile(filePath)
|
||||
formData.append('file', file)
|
||||
|
||||
const response = await restClient.POST(`/${mediaSlug}`, {
|
||||
body: formData,
|
||||
file: true,
|
||||
file,
|
||||
})
|
||||
const { doc } = await response.json()
|
||||
|
||||
await handle.close()
|
||||
|
||||
expect(response.status).toBe(201)
|
||||
|
||||
const { sizes } = doc
|
||||
@@ -130,16 +83,20 @@ describe('Collections - Uploads', () => {
|
||||
})
|
||||
|
||||
it('creates from form data given an svg', async () => {
|
||||
const formData = new FormData()
|
||||
const filePath = path.join(dirname, './image.svg')
|
||||
formData.append('file', await bufferToFileBlob(filePath))
|
||||
const formData = new FormData()
|
||||
const { file, handle } = await createStreamableFile(filePath)
|
||||
formData.append('file', file)
|
||||
|
||||
const response = await restClient.POST(`/${mediaSlug}`, {
|
||||
body: formData,
|
||||
file: true,
|
||||
file,
|
||||
})
|
||||
|
||||
const { doc } = await response.json()
|
||||
|
||||
await handle.close()
|
||||
|
||||
expect(response.status).toBe(201)
|
||||
|
||||
// Check for files
|
||||
@@ -151,100 +108,426 @@ describe('Collections - Uploads', () => {
|
||||
expect(doc.width).toBeDefined()
|
||||
expect(doc.height).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
it('should have valid image url', async () => {
|
||||
const formData = new FormData()
|
||||
const fileBlob = await bufferToFileBlob(path.join(dirname, './image.svg'))
|
||||
formData.append('file', fileBlob)
|
||||
it('should have valid image url', async () => {
|
||||
const formData = new FormData()
|
||||
const filePath = path.join(dirname, './image.svg')
|
||||
const { file, handle } = await createStreamableFile(filePath)
|
||||
formData.append('file', file)
|
||||
|
||||
const response = await restClient.POST(`/${mediaSlug}`, {
|
||||
body: formData,
|
||||
file: true,
|
||||
const response = await restClient.POST(`/${mediaSlug}`, {
|
||||
body: formData,
|
||||
file,
|
||||
})
|
||||
const { doc } = await response.json()
|
||||
|
||||
await handle.close()
|
||||
|
||||
expect(response.status).toBe(201)
|
||||
const expectedPath = path.join(dirname, './media')
|
||||
expect(await fileExists(path.join(expectedPath, doc.filename))).toBe(true)
|
||||
|
||||
expect(doc.url).not.toContain('undefined')
|
||||
})
|
||||
const { doc } = await response.json()
|
||||
|
||||
expect(response.status).toBe(201)
|
||||
const expectedPath = path.join(dirname, './media')
|
||||
expect(await fileExists(path.join(expectedPath, doc.filename))).toBe(true)
|
||||
it('creates images that do not require all sizes', async () => {
|
||||
const formData = new FormData()
|
||||
const filePath = path.join(dirname, './small.png')
|
||||
const { file, handle } = await createStreamableFile(filePath)
|
||||
formData.append('file', file)
|
||||
|
||||
expect(doc.url).not.toContain('undefined')
|
||||
})
|
||||
const response = await restClient.POST(`/${mediaSlug}`, {
|
||||
body: formData,
|
||||
file,
|
||||
})
|
||||
const { doc } = await response.json()
|
||||
|
||||
it('creates images that do not require all sizes', async () => {
|
||||
const formData = new FormData()
|
||||
const fileBlob = await bufferToFileBlob(path.join(dirname, './small.png'))
|
||||
formData.append('file', fileBlob)
|
||||
await handle.close()
|
||||
|
||||
const response = await restClient.POST(`/${mediaSlug}`, {
|
||||
body: formData,
|
||||
file: true,
|
||||
expect(response.status).toBe(201)
|
||||
|
||||
const expectedPath = path.join(dirname, './media')
|
||||
|
||||
// Check for files
|
||||
expect(await fileExists(path.join(expectedPath, doc.filename))).toBe(true)
|
||||
expect(await fileExists(path.join(expectedPath, 'small-640x480.png'))).toBe(false)
|
||||
expect(await fileExists(path.join(expectedPath, doc.sizes.icon.filename))).toBe(true)
|
||||
|
||||
// Check api response
|
||||
expect(doc.sizes.tablet.filename).toBeNull()
|
||||
expect(doc.sizes.icon.filename).toBeDefined()
|
||||
})
|
||||
const { doc } = await response.json()
|
||||
|
||||
expect(response.status).toBe(201)
|
||||
it('creates images from a different format', async () => {
|
||||
const formData = new FormData()
|
||||
const filePath = path.join(dirname, './image.jpg')
|
||||
const { file, handle } = await createStreamableFile(filePath)
|
||||
formData.append('file', file)
|
||||
|
||||
const expectedPath = path.join(dirname, './media')
|
||||
const response = await restClient.POST(`/${mediaSlug}`, {
|
||||
body: formData,
|
||||
file,
|
||||
})
|
||||
const { doc } = await response.json()
|
||||
|
||||
// Check for files
|
||||
expect(await fileExists(path.join(expectedPath, doc.filename))).toBe(true)
|
||||
expect(await fileExists(path.join(expectedPath, 'small-640x480.png'))).toBe(false)
|
||||
expect(await fileExists(path.join(expectedPath, doc.sizes.icon.filename))).toBe(true)
|
||||
await handle.close()
|
||||
|
||||
// Check api response
|
||||
expect(doc.sizes.tablet.filename).toBeNull()
|
||||
expect(doc.sizes.icon.filename).toBeDefined()
|
||||
})
|
||||
expect(response.status).toBe(201)
|
||||
|
||||
it('creates images from a different format', async () => {
|
||||
const formData = new FormData()
|
||||
const fileBlob = await bufferToFileBlob(path.join(dirname, './image.jpg'))
|
||||
formData.append('file', fileBlob)
|
||||
const expectedPath = path.join(dirname, './media')
|
||||
|
||||
const response = await restClient.POST(`/${mediaSlug}`, {
|
||||
body: formData,
|
||||
file: true,
|
||||
// Check for files
|
||||
expect(await fileExists(path.join(expectedPath, doc.filename))).toBe(true)
|
||||
expect(await fileExists(path.join(expectedPath, doc.sizes.tablet.filename))).toBe(true)
|
||||
|
||||
// Check api response
|
||||
expect(doc.filename).toContain('.png')
|
||||
expect(doc.mimeType).toEqual('image/png')
|
||||
expect(doc.sizes.maintainedAspectRatio.filename).toContain('.png')
|
||||
expect(doc.sizes.maintainedAspectRatio.mimeType).toContain('image/png')
|
||||
expect(doc.sizes.differentFormatFromMainImage.filename).toContain('.jpg')
|
||||
expect(doc.sizes.differentFormatFromMainImage.mimeType).toContain('image/jpeg')
|
||||
})
|
||||
const { doc } = await response.json()
|
||||
|
||||
expect(response.status).toBe(201)
|
||||
it('creates media without storing a file', async () => {
|
||||
const formData = new FormData()
|
||||
const filePath = path.join(dirname, './unstored.png')
|
||||
const { file, handle } = await createStreamableFile(filePath)
|
||||
formData.append('file', file)
|
||||
|
||||
const expectedPath = path.join(dirname, './media')
|
||||
// unstored media
|
||||
const response = await restClient.POST(`/${unstoredMediaSlug}`, {
|
||||
body: formData,
|
||||
file,
|
||||
})
|
||||
const { doc } = await response.json()
|
||||
|
||||
// Check for files
|
||||
expect(await fileExists(path.join(expectedPath, doc.filename))).toBe(true)
|
||||
expect(await fileExists(path.join(expectedPath, doc.sizes.tablet.filename))).toBe(true)
|
||||
await handle.close()
|
||||
|
||||
// Check api response
|
||||
expect(doc.filename).toContain('.png')
|
||||
expect(doc.mimeType).toEqual('image/png')
|
||||
expect(doc.sizes.maintainedAspectRatio.filename).toContain('.png')
|
||||
expect(doc.sizes.maintainedAspectRatio.mimeType).toContain('image/png')
|
||||
expect(doc.sizes.differentFormatFromMainImage.filename).toContain('.jpg')
|
||||
expect(doc.sizes.differentFormatFromMainImage.mimeType).toContain('image/jpeg')
|
||||
})
|
||||
expect(response.status).toBe(201)
|
||||
|
||||
it('creates media without storing a file', async () => {
|
||||
const formData = new FormData()
|
||||
const fileBlob = await bufferToFileBlob(path.join(dirname, './unstored.png'))
|
||||
formData.append('file', fileBlob)
|
||||
// Check for files
|
||||
expect(await fileExists(path.join(dirname, './media', doc.filename))).toBe(false)
|
||||
|
||||
// unstored media
|
||||
const response = await restClient.POST(`/${unstoredMediaSlug}`, {
|
||||
body: formData,
|
||||
file: true,
|
||||
// Check api response
|
||||
expect(doc.filename).toBeDefined()
|
||||
})
|
||||
const { doc } = await response.json()
|
||||
|
||||
expect(response.status).toBe(201)
|
||||
|
||||
// Check for files
|
||||
expect(await fileExists(path.join(dirname, './media', doc.filename))).toBe(false)
|
||||
|
||||
// Check api response
|
||||
expect(doc.filename).toBeDefined()
|
||||
})
|
||||
|
||||
describe('update', () => {
|
||||
it('should replace image and delete old files - by ID', async () => {
|
||||
const filePath = path.resolve(dirname, './image.png')
|
||||
const file = await getFileByPath(filePath)
|
||||
file.name = 'renamed.png'
|
||||
|
||||
const mediaDoc = (await payload.create({
|
||||
collection: mediaSlug,
|
||||
data: {},
|
||||
file,
|
||||
})) as unknown as Media
|
||||
|
||||
const formData = new FormData()
|
||||
const filePath2 = path.resolve(dirname, './small.png')
|
||||
const { file: file2, handle } = await createStreamableFile(filePath2)
|
||||
formData.append('file', file2)
|
||||
|
||||
const response = await restClient.PATCH(`/${mediaSlug}/${mediaDoc.id}`, {
|
||||
body: formData,
|
||||
file: file2,
|
||||
})
|
||||
|
||||
await handle.close()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
const expectedPath = path.join(dirname, './media')
|
||||
|
||||
// Check that previously existing files were removed
|
||||
expect(await fileExists(path.join(expectedPath, mediaDoc.filename))).toBe(false)
|
||||
expect(await fileExists(path.join(expectedPath, mediaDoc.sizes.icon.filename))).toBe(false)
|
||||
})
|
||||
|
||||
it('should replace image and delete old files - where query', async () => {
|
||||
const filePath = path.resolve(dirname, './image.png')
|
||||
const file = await getFileByPath(filePath)
|
||||
file.name = 'renamed.png'
|
||||
|
||||
const mediaDoc = (await payload.create({
|
||||
collection: mediaSlug,
|
||||
data: {},
|
||||
file,
|
||||
})) as unknown as Media
|
||||
|
||||
const formData = new FormData()
|
||||
const filePath2 = path.resolve(dirname, './small.png')
|
||||
const { file: file2, handle } = await createStreamableFile(filePath2)
|
||||
formData.append('file', file2)
|
||||
|
||||
const response = await restClient.PATCH(`/${mediaSlug}`, {
|
||||
body: formData,
|
||||
file: file2,
|
||||
query: {
|
||||
where: {
|
||||
id: {
|
||||
equals: mediaDoc.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await handle.close()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
const expectedPath = path.join(dirname, './media')
|
||||
|
||||
// Check that previously existing files were removed
|
||||
expect(await fileExists(path.join(expectedPath, mediaDoc.filename))).toBe(false)
|
||||
expect(await fileExists(path.join(expectedPath, mediaDoc.sizes.icon.filename))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete', () => {
|
||||
it('should remove related files when deleting by ID', async () => {
|
||||
const formData = new FormData()
|
||||
const filePath = path.join(dirname, './image.png')
|
||||
const { file, handle } = await createStreamableFile(filePath)
|
||||
|
||||
formData.append('file', file)
|
||||
|
||||
const { doc } = await restClient
|
||||
.POST(`/${mediaSlug}`, {
|
||||
body: formData,
|
||||
file,
|
||||
})
|
||||
.then((res) => res.json())
|
||||
|
||||
await handle.close()
|
||||
|
||||
const response2 = await restClient.DELETE(`/${mediaSlug}/${doc.id}`)
|
||||
expect(response2.status).toBe(200)
|
||||
|
||||
expect(await fileExists(path.join(dirname, doc.filename))).toBe(false)
|
||||
})
|
||||
|
||||
it('should remove all related files when deleting with where query', async () => {
|
||||
const formData = new FormData()
|
||||
const filePath = path.join(dirname, './image.png')
|
||||
const { file, handle } = await createStreamableFile(filePath)
|
||||
formData.append('file', file)
|
||||
|
||||
const { doc } = await restClient
|
||||
.POST(`/${mediaSlug}`, {
|
||||
body: formData,
|
||||
file,
|
||||
})
|
||||
.then((res) => res.json())
|
||||
|
||||
await handle.close()
|
||||
|
||||
const { errors } = await restClient
|
||||
.DELETE(`/${mediaSlug}`, {
|
||||
query: {
|
||||
where: {
|
||||
id: {
|
||||
equals: doc.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
|
||||
expect(errors).toHaveLength(0)
|
||||
|
||||
expect(await fileExists(path.join(dirname, doc.filename))).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Local API', () => {
|
||||
describe('update', () => {
|
||||
it('should remove existing media on re-upload - by ID', async () => {
|
||||
// Create temp file
|
||||
const filePath = path.resolve(dirname, './temp.png')
|
||||
const file = await getFileByPath(filePath)
|
||||
file.name = 'temp.png'
|
||||
|
||||
const mediaDoc = (await payload.create({
|
||||
collection: mediaSlug,
|
||||
data: {},
|
||||
file,
|
||||
})) as unknown as Media
|
||||
|
||||
const expectedPath = path.join(dirname, './media')
|
||||
|
||||
// Check that the temp file was created
|
||||
expect(await fileExists(path.join(expectedPath, mediaDoc.filename))).toBe(true)
|
||||
|
||||
// Replace the temp file with a new one
|
||||
const newFilePath = path.resolve(dirname, './temp-renamed.png')
|
||||
const newFile = await getFileByPath(newFilePath)
|
||||
newFile.name = 'temp-renamed.png'
|
||||
|
||||
const updatedMediaDoc = (await payload.update({
|
||||
collection: mediaSlug,
|
||||
id: mediaDoc.id,
|
||||
file: newFile,
|
||||
data: {},
|
||||
})) as unknown as Media
|
||||
|
||||
// Check that the replacement file was created and the old one was removed
|
||||
expect(await fileExists(path.join(expectedPath, updatedMediaDoc.filename))).toBe(true)
|
||||
expect(await fileExists(path.join(expectedPath, mediaDoc.filename))).toBe(false)
|
||||
})
|
||||
|
||||
it('should remove existing media on re-upload - where query', async () => {
|
||||
// Create temp file
|
||||
const filePath = path.resolve(dirname, './temp.png')
|
||||
const file = await getFileByPath(filePath)
|
||||
file.name = 'temp.png'
|
||||
|
||||
const mediaDoc = (await payload.create({
|
||||
collection: mediaSlug,
|
||||
data: {},
|
||||
file,
|
||||
})) as unknown as Media
|
||||
|
||||
const expectedPath = path.join(dirname, './media')
|
||||
|
||||
// Check that the temp file was created
|
||||
expect(await fileExists(path.join(expectedPath, mediaDoc.filename))).toBe(true)
|
||||
|
||||
// Replace the temp file with a new one
|
||||
const newFilePath = path.resolve(dirname, './temp-renamed.png')
|
||||
const newFile = await getFileByPath(newFilePath)
|
||||
newFile.name = 'temp-renamed-second.png'
|
||||
|
||||
const updatedMediaDoc = (await payload.update({
|
||||
collection: mediaSlug,
|
||||
where: {
|
||||
id: { equals: mediaDoc.id },
|
||||
},
|
||||
file: newFile,
|
||||
data: {},
|
||||
})) as unknown as { docs: Media[] }
|
||||
|
||||
// Check that the replacement file was created and the old one was removed
|
||||
expect(updatedMediaDoc.docs[0].filename).toEqual(newFile.name)
|
||||
expect(await fileExists(path.join(expectedPath, updatedMediaDoc.docs[0].filename))).toBe(
|
||||
true,
|
||||
)
|
||||
expect(await fileExists(path.join(expectedPath, mediaDoc.filename))).toBe(false)
|
||||
})
|
||||
|
||||
it('should remove sizes that do not pertain to the new image - by ID', async () => {
|
||||
const filePath = path.resolve(dirname, './image.png')
|
||||
const file = await getFileByPath(filePath)
|
||||
const small = await getFileByPath(path.resolve(dirname, './small.png'))
|
||||
|
||||
const { id } = await payload.create({
|
||||
collection: mediaSlug,
|
||||
data: {},
|
||||
file,
|
||||
})
|
||||
|
||||
const doc = (await payload.update({
|
||||
collection: mediaSlug,
|
||||
id,
|
||||
data: {},
|
||||
file: small,
|
||||
})) as unknown as Media
|
||||
|
||||
expect(doc.sizes.icon).toBeDefined()
|
||||
expect(doc.sizes.tablet.width).toBeNull()
|
||||
})
|
||||
|
||||
it('should remove sizes that do not pertain to the new image - where query', async () => {
|
||||
const filePath = path.resolve(dirname, './image.png')
|
||||
const file = await getFileByPath(filePath)
|
||||
const small = await getFileByPath(path.resolve(dirname, './small.png'))
|
||||
|
||||
const { id } = await payload.create({
|
||||
collection: mediaSlug,
|
||||
data: {},
|
||||
file,
|
||||
})
|
||||
|
||||
const doc = (await payload.update({
|
||||
collection: mediaSlug,
|
||||
where: {
|
||||
id: { equals: id },
|
||||
},
|
||||
data: {},
|
||||
file: small,
|
||||
})) as unknown as { docs: Media[] }
|
||||
|
||||
expect(doc.docs[0].sizes.icon).toBeDefined()
|
||||
expect(doc.docs[0].sizes.tablet.width).toBeNull()
|
||||
})
|
||||
|
||||
it('should allow removing file from upload relationship field - by ID', async () => {
|
||||
const filePath = path.resolve(dirname, './image.png')
|
||||
const file = await getFileByPath(filePath)
|
||||
file.name = 'renamed.png'
|
||||
|
||||
const { id } = await payload.create({
|
||||
collection: mediaSlug,
|
||||
data: {},
|
||||
file,
|
||||
})
|
||||
|
||||
const related = await payload.create({
|
||||
collection: relationSlug,
|
||||
data: {
|
||||
image: id,
|
||||
},
|
||||
})
|
||||
|
||||
const doc = await payload.update({
|
||||
collection: relationSlug,
|
||||
id: related.id,
|
||||
data: {
|
||||
image: null,
|
||||
},
|
||||
})
|
||||
|
||||
expect(doc.image).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should allow update removing a relationship - where query', async () => {
|
||||
const filePath = path.resolve(dirname, './image.png')
|
||||
const file = await getFileByPath(filePath)
|
||||
file.name = 'renamed.png'
|
||||
|
||||
const { id } = await payload.create({
|
||||
collection: mediaSlug,
|
||||
data: {},
|
||||
file,
|
||||
})
|
||||
|
||||
const related = await payload.create({
|
||||
collection: relationSlug,
|
||||
data: {
|
||||
image: id,
|
||||
},
|
||||
})
|
||||
|
||||
const doc = await payload.update({
|
||||
collection: relationSlug,
|
||||
where: {
|
||||
id: { equals: related.id },
|
||||
},
|
||||
data: {
|
||||
image: null,
|
||||
},
|
||||
})
|
||||
|
||||
expect(doc.docs[0].image).toBeFalsy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Image Manipulation', () => {
|
||||
it('should enlarge images if resize options `withoutEnlargement` is set to false', async () => {
|
||||
const small = await getFileByPath(path.resolve(dirname, './small.png'))
|
||||
|
||||
@@ -318,8 +601,6 @@ describe('Collections - Uploads', () => {
|
||||
})
|
||||
|
||||
it('should not reduce images if resize options `withoutReduction` is set to true', async () => {
|
||||
const formData = new NodeFormData()
|
||||
formData.append('file', fs.createReadStream(path.join(dirname, './small.png')))
|
||||
const small = await getFileByPath(path.resolve(dirname, './small.png'))
|
||||
|
||||
const result = await payload.create({
|
||||
@@ -359,294 +640,7 @@ describe('Collections - Uploads', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('update', async () => {
|
||||
// Create image
|
||||
const filePath = path.resolve(dirname, './image.png')
|
||||
const file = await getFileByPath(filePath)
|
||||
file.name = 'renamed.png'
|
||||
|
||||
const mediaDoc = (await payload.create({
|
||||
collection: mediaSlug,
|
||||
data: {},
|
||||
file,
|
||||
})) as unknown as Media
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', await bufferToFileBlob(path.join(dirname, './small.png')))
|
||||
|
||||
const response = await restClient.PATCH(`/${mediaSlug}/${mediaDoc.id}`, {
|
||||
body: formData,
|
||||
file: true,
|
||||
})
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
const expectedPath = path.join(dirname, './media')
|
||||
|
||||
// Check that previously existing files were removed
|
||||
expect(await fileExists(path.join(expectedPath, mediaDoc.filename))).toBe(false)
|
||||
expect(await fileExists(path.join(expectedPath, mediaDoc.sizes.icon.filename))).toBe(false)
|
||||
})
|
||||
|
||||
it('update - update many', async () => {
|
||||
// Create image
|
||||
const filePath = path.resolve(dirname, './image.png')
|
||||
const file = await getFileByPath(filePath)
|
||||
file.name = 'renamed.png'
|
||||
|
||||
const mediaDoc = (await payload.create({
|
||||
collection: mediaSlug,
|
||||
data: {},
|
||||
file,
|
||||
})) as unknown as Media
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', await bufferToFileBlob(path.join(dirname, './small.png')))
|
||||
|
||||
const response = await restClient.PATCH(`/${mediaSlug}`, {
|
||||
body: formData,
|
||||
file: true,
|
||||
query: {
|
||||
where: {
|
||||
id: {
|
||||
equals: mediaDoc.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
const expectedPath = path.join(dirname, './media')
|
||||
|
||||
// Check that previously existing files were removed
|
||||
expect(await fileExists(path.join(expectedPath, mediaDoc.filename))).toBe(false)
|
||||
expect(await fileExists(path.join(expectedPath, mediaDoc.sizes.icon.filename))).toBe(false)
|
||||
})
|
||||
|
||||
it('should remove existing media on re-upload', async () => {
|
||||
// Create temp file
|
||||
const filePath = path.resolve(dirname, './temp.png')
|
||||
const file = await getFileByPath(filePath)
|
||||
file.name = 'temp.png'
|
||||
|
||||
const mediaDoc = (await payload.create({
|
||||
collection: mediaSlug,
|
||||
data: {},
|
||||
file,
|
||||
})) as unknown as Media
|
||||
|
||||
const expectedPath = path.join(dirname, './media')
|
||||
|
||||
// Check that the temp file was created
|
||||
expect(await fileExists(path.join(expectedPath, mediaDoc.filename))).toBe(true)
|
||||
|
||||
// Replace the temp file with a new one
|
||||
const newFilePath = path.resolve(dirname, './temp-renamed.png')
|
||||
const newFile = await getFileByPath(newFilePath)
|
||||
newFile.name = 'temp-renamed.png'
|
||||
|
||||
const updatedMediaDoc = (await payload.update({
|
||||
collection: mediaSlug,
|
||||
id: mediaDoc.id,
|
||||
file: newFile,
|
||||
data: {},
|
||||
})) as unknown as Media
|
||||
|
||||
// Check that the replacement file was created and the old one was removed
|
||||
expect(await fileExists(path.join(expectedPath, updatedMediaDoc.filename))).toBe(true)
|
||||
expect(await fileExists(path.join(expectedPath, mediaDoc.filename))).toBe(false)
|
||||
})
|
||||
|
||||
it('should remove existing media on re-upload - update many', async () => {
|
||||
// Create temp file
|
||||
const filePath = path.resolve(dirname, './temp.png')
|
||||
const file = await getFileByPath(filePath)
|
||||
file.name = 'temp.png'
|
||||
|
||||
const mediaDoc = (await payload.create({
|
||||
collection: mediaSlug,
|
||||
data: {},
|
||||
file,
|
||||
})) as unknown as Media
|
||||
|
||||
const expectedPath = path.join(dirname, './media')
|
||||
|
||||
// Check that the temp file was created
|
||||
expect(await fileExists(path.join(expectedPath, mediaDoc.filename))).toBe(true)
|
||||
|
||||
// Replace the temp file with a new one
|
||||
const newFilePath = path.resolve(dirname, './temp-renamed.png')
|
||||
const newFile = await getFileByPath(newFilePath)
|
||||
newFile.name = 'temp-renamed-second.png'
|
||||
|
||||
const updatedMediaDoc = (await payload.update({
|
||||
collection: mediaSlug,
|
||||
where: {
|
||||
id: { equals: mediaDoc.id },
|
||||
},
|
||||
file: newFile,
|
||||
data: {},
|
||||
})) as unknown as { docs: Media[] }
|
||||
|
||||
// Check that the replacement file was created and the old one was removed
|
||||
expect(updatedMediaDoc.docs[0].filename).toEqual(newFile.name)
|
||||
expect(await fileExists(path.join(expectedPath, updatedMediaDoc.docs[0].filename))).toBe(true)
|
||||
expect(await fileExists(path.join(expectedPath, mediaDoc.filename))).toBe(false)
|
||||
})
|
||||
|
||||
it('should remove extra sizes on update', async () => {
|
||||
const filePath = path.resolve(dirname, './image.png')
|
||||
const file = await getFileByPath(filePath)
|
||||
const small = await getFileByPath(path.resolve(dirname, './small.png'))
|
||||
|
||||
const { id } = await payload.create({
|
||||
collection: mediaSlug,
|
||||
data: {},
|
||||
file,
|
||||
})
|
||||
|
||||
const doc = (await payload.update({
|
||||
collection: mediaSlug,
|
||||
id,
|
||||
data: {},
|
||||
file: small,
|
||||
})) as unknown as Media
|
||||
|
||||
expect(doc.sizes.icon).toBeDefined()
|
||||
expect(doc.sizes.tablet.width).toBeNull()
|
||||
})
|
||||
|
||||
it('should remove extra sizes on update - update many', async () => {
|
||||
const filePath = path.resolve(dirname, './image.png')
|
||||
const file = await getFileByPath(filePath)
|
||||
const small = await getFileByPath(path.resolve(dirname, './small.png'))
|
||||
|
||||
const { id } = await payload.create({
|
||||
collection: mediaSlug,
|
||||
data: {},
|
||||
file,
|
||||
})
|
||||
|
||||
const doc = (await payload.update({
|
||||
collection: mediaSlug,
|
||||
where: {
|
||||
id: { equals: id },
|
||||
},
|
||||
data: {},
|
||||
file: small,
|
||||
})) as unknown as { docs: Media[] }
|
||||
|
||||
expect(doc.docs[0].sizes.icon).toBeDefined()
|
||||
expect(doc.docs[0].sizes.tablet.width).toBeNull()
|
||||
})
|
||||
|
||||
it('should allow update removing a relationship', async () => {
|
||||
const filePath = path.resolve(dirname, './image.png')
|
||||
const file = await getFileByPath(filePath)
|
||||
file.name = 'renamed.png'
|
||||
|
||||
const { id } = await payload.create({
|
||||
collection: mediaSlug,
|
||||
data: {},
|
||||
file,
|
||||
})
|
||||
|
||||
const related = await payload.create({
|
||||
collection: relationSlug,
|
||||
data: {
|
||||
image: id,
|
||||
},
|
||||
})
|
||||
|
||||
const doc = await payload.update({
|
||||
collection: relationSlug,
|
||||
id: related.id,
|
||||
data: {
|
||||
image: null,
|
||||
},
|
||||
})
|
||||
|
||||
expect(doc.image).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should allow update removing a relationship - update many', async () => {
|
||||
const filePath = path.resolve(dirname, './image.png')
|
||||
const file = await getFileByPath(filePath)
|
||||
file.name = 'renamed.png'
|
||||
|
||||
const { id } = await payload.create({
|
||||
collection: mediaSlug,
|
||||
data: {},
|
||||
file,
|
||||
})
|
||||
|
||||
const related = await payload.create({
|
||||
collection: relationSlug,
|
||||
data: {
|
||||
image: id,
|
||||
},
|
||||
})
|
||||
|
||||
const doc = await payload.update({
|
||||
collection: relationSlug,
|
||||
where: {
|
||||
id: { equals: related.id },
|
||||
},
|
||||
data: {
|
||||
image: null,
|
||||
},
|
||||
})
|
||||
|
||||
expect(doc.docs[0].image).toBeFalsy()
|
||||
})
|
||||
|
||||
it('delete', async () => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', await bufferToFileBlob(path.join(dirname, './image.png')))
|
||||
|
||||
const { doc } = await restClient
|
||||
.POST(`/${mediaSlug}`, {
|
||||
body: formData,
|
||||
file: true,
|
||||
})
|
||||
.then((res) => res.json())
|
||||
|
||||
const response2 = await restClient.DELETE(`/${mediaSlug}/${doc.id}`)
|
||||
expect(response2.status).toBe(200)
|
||||
|
||||
expect(await fileExists(path.join(dirname, doc.filename))).toBe(false)
|
||||
})
|
||||
|
||||
it('delete - update many', async () => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', await bufferToFileBlob(path.join(dirname, './image.png')))
|
||||
|
||||
const { doc } = await restClient
|
||||
.POST(`/${mediaSlug}`, {
|
||||
body: formData,
|
||||
file: true,
|
||||
})
|
||||
.then((res) => res.json())
|
||||
|
||||
const { errors } = await restClient
|
||||
.DELETE(`/${mediaSlug}`, {
|
||||
query: {
|
||||
where: {
|
||||
id: {
|
||||
equals: doc.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
|
||||
expect(errors).toHaveLength(0)
|
||||
|
||||
expect(await fileExists(path.join(dirname, doc.filename))).toBe(false)
|
||||
})
|
||||
|
||||
describe('filesRequiredOnCreate', () => {
|
||||
describe('Required Files', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
it('should allow file to be optional if filesRequiredOnCreate is false', async () => {
|
||||
const successfulCreate = await payload.create({
|
||||
|
||||
Reference in New Issue
Block a user