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) => {
|
export const memHandler: Handler = (options, fieldname, filename) => {
|
||||||
const buffers = []
|
const buffers: Buffer[] = []
|
||||||
const hash = crypto.createHash('md5')
|
const hash = crypto.createHash('md5')
|
||||||
let fileSize = 0
|
let fileSize = 0
|
||||||
let completed = false
|
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 UNACCEPTABLE_METHODS = new Set(['GET', 'HEAD', 'DELETE', 'OPTIONS', 'CONNECT', 'TRACE'])
|
||||||
|
|
||||||
const hasBody = (req: Request): boolean => {
|
const hasBody = (req: Request): boolean => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Busboy from 'busboy'
|
import Busboy from 'busboy'
|
||||||
|
import httpStatus from 'http-status'
|
||||||
import { APIError } from 'payload/errors'
|
import { APIError } from 'payload/errors'
|
||||||
|
|
||||||
import type { NextFileUploadOptions, NextFileUploadResponse } from './index.js'
|
import type { NextFileUploadOptions, NextFileUploadResponse } from './index.js'
|
||||||
@@ -17,6 +18,17 @@ type ProcessMultipart = (args: {
|
|||||||
}) => Promise<NextFileUploadResponse>
|
}) => Promise<NextFileUploadResponse>
|
||||||
export const processMultipart: ProcessMultipart = async ({ options, request }) => {
|
export const processMultipart: ProcessMultipart = async ({ options, request }) => {
|
||||||
let parsingRequest = true
|
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 = {
|
const result: NextFileUploadResponse = {
|
||||||
fields: undefined,
|
fields: undefined,
|
||||||
files: undefined,
|
files: undefined,
|
||||||
@@ -36,6 +48,7 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
|||||||
|
|
||||||
// Build req.files fields
|
// Build req.files fields
|
||||||
busboy.on('file', (field, file, info) => {
|
busboy.on('file', (field, file, info) => {
|
||||||
|
fileCount += 1
|
||||||
// Parse file name(cutting huge names, decoding, etc..).
|
// Parse file name(cutting huge names, decoding, etc..).
|
||||||
const { encoding, filename: name, mimeType: mime } = info
|
const { encoding, filename: name, mimeType: mime } = info
|
||||||
const filename = parseFileName(options, name)
|
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}.`)
|
debugLog(options, `Aborting upload because of size limit ${field}->${filename}.`)
|
||||||
cleanup()
|
cleanup()
|
||||||
parsingRequest = false
|
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`)
|
return debugLog(options, `Don't add file instance if original name and size are empty`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filesCompleted += 1
|
||||||
|
|
||||||
result.files = buildFields(
|
result.files = buildFields(
|
||||||
result.files,
|
result.files,
|
||||||
field,
|
field,
|
||||||
@@ -117,19 +134,25 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
|||||||
request[waitFlushProperty] = []
|
request[waitFlushProperty] = []
|
||||||
}
|
}
|
||||||
request[waitFlushProperty].push(writePromise)
|
request[waitFlushProperty].push(writePromise)
|
||||||
|
|
||||||
|
if (filesCompleted === fileCount) {
|
||||||
|
allFilesHaveResolved()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
file.on('error', (err) => {
|
file.on('error', (err) => {
|
||||||
uploadTimer.clear()
|
uploadTimer.clear()
|
||||||
debugLog(options, `File Error: ${err.message}`)
|
debugLog(options, `File Error: ${err.message}`)
|
||||||
cleanup()
|
cleanup()
|
||||||
|
failedResolvingFiles(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Start upload process.
|
||||||
debugLog(options, `New upload started ${field}->${filename}, bytes:${getFileSize()}`)
|
debugLog(options, `New upload started ${field}->${filename}, bytes:${getFileSize()}`)
|
||||||
uploadTimer.set()
|
uploadTimer.set()
|
||||||
})
|
})
|
||||||
|
|
||||||
busboy.on('finish', () => {
|
busboy.on('finish', async () => {
|
||||||
debugLog(options, `Busboy finished parsing request.`)
|
debugLog(options, `Busboy finished parsing request.`)
|
||||||
if (options.parseNested) {
|
if (options.parseNested) {
|
||||||
result.fields = processNested(result.fields)
|
result.fields = processNested(result.fields)
|
||||||
@@ -137,20 +160,27 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (request[waitFlushProperty]) {
|
if (request[waitFlushProperty]) {
|
||||||
Promise.all(request[waitFlushProperty]).then(() => {
|
try {
|
||||||
|
await Promise.all(request[waitFlushProperty]).then(() => {
|
||||||
delete request[waitFlushProperty]
|
delete request[waitFlushProperty]
|
||||||
})
|
})
|
||||||
|
} catch (err) {
|
||||||
|
debugLog(options, `Error waiting for file write promises: ${err}`)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
busboy.on('error', (err) => {
|
busboy.on('error', (err) => {
|
||||||
debugLog(options, `Busboy error`)
|
debugLog(options, `Busboy error`)
|
||||||
parsingRequest = false
|
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()
|
const reader = request.body.getReader()
|
||||||
|
|
||||||
|
// Start parsing request
|
||||||
while (parsingRequest) {
|
while (parsingRequest) {
|
||||||
const { done, value } = await reader.read()
|
const { done, value } = await reader.read()
|
||||||
|
|
||||||
@@ -163,5 +193,7 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fileCount !== 0) await allFilesComplete
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ let tempCounter = 0
|
|||||||
export const debugLog = (options: NextFileUploadOptions, msg: string) => {
|
export const debugLog = (options: NextFileUploadOptions, msg: string) => {
|
||||||
const opts = options || {}
|
const opts = options || {}
|
||||||
if (!opts.debug) return false
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,8 +287,9 @@ export const parseFileName: ParseFileName = (opts, fileName) => {
|
|||||||
? opts.safeFileNames
|
? opts.safeFileNames
|
||||||
: SAFE_FILE_NAME_REGEX
|
: SAFE_FILE_NAME_REGEX
|
||||||
// Parse file name extension.
|
// Parse file name extension.
|
||||||
let { name, extension } = parseFileNameExtension(opts.preserveExtension, parsedName)
|
const parsedFileName = parseFileNameExtension(opts.preserveExtension, parsedName)
|
||||||
if (extension.length) extension = '.' + extension.replace(nameRegex, '')
|
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,
|
token,
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
routeError({
|
return routeError({
|
||||||
collection,
|
collection,
|
||||||
|
config: req.payload.config,
|
||||||
err,
|
err,
|
||||||
req,
|
req,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export const getFile = async ({ collection, filename, req }: Args): Promise<Resp
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return routeError({
|
return routeError({
|
||||||
collection,
|
collection,
|
||||||
|
config: req.payload.config,
|
||||||
err: error,
|
err: error,
|
||||||
req,
|
req,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ export const preview: GlobalRouteHandler = async ({ globalConfig, req }) => {
|
|||||||
token,
|
token,
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
routeError({
|
return routeError({
|
||||||
|
config: req.payload.config,
|
||||||
err,
|
err,
|
||||||
req,
|
req,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -303,6 +303,7 @@ export const GET =
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return routeError({
|
return routeError({
|
||||||
collection,
|
collection,
|
||||||
|
config,
|
||||||
err: error,
|
err: error,
|
||||||
req,
|
req,
|
||||||
})
|
})
|
||||||
@@ -445,6 +446,7 @@ export const POST =
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return routeError({
|
return routeError({
|
||||||
collection,
|
collection,
|
||||||
|
config,
|
||||||
err: error,
|
err: error,
|
||||||
req,
|
req,
|
||||||
})
|
})
|
||||||
@@ -514,6 +516,7 @@ export const DELETE =
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return routeError({
|
return routeError({
|
||||||
collection,
|
collection,
|
||||||
|
config,
|
||||||
err: error,
|
err: error,
|
||||||
req,
|
req,
|
||||||
})
|
})
|
||||||
@@ -583,6 +586,7 @@ export const PATCH =
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return routeError({
|
return routeError({
|
||||||
collection,
|
collection,
|
||||||
|
config,
|
||||||
err: error,
|
err: error,
|
||||||
req,
|
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 httpStatus from 'http-status'
|
||||||
import { APIError } from 'payload/errors'
|
import { APIError } from 'payload/errors'
|
||||||
|
|
||||||
|
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
|
||||||
|
|
||||||
export type ErrorResponse = { data?: any; errors: unknown[]; stack?: string }
|
export type ErrorResponse = { data?: any; errors: unknown[]; stack?: string }
|
||||||
|
|
||||||
const formatErrors = (incoming: { [key: string]: unknown } | APIError): ErrorResponse => {
|
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,
|
collection,
|
||||||
|
config: configArg,
|
||||||
err,
|
err,
|
||||||
req,
|
req,
|
||||||
}: {
|
}: {
|
||||||
collection?: Collection
|
collection?: Collection
|
||||||
|
config: Promise<SanitizedConfig> | SanitizedConfig
|
||||||
err: APIError
|
err: APIError
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
}) => {
|
}) => {
|
||||||
if (!req?.payload) {
|
let payload = req?.payload
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
try {
|
||||||
|
payload = await getPayloadHMR({ config: configArg })
|
||||||
|
} catch (e) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{
|
{
|
||||||
message: err.message,
|
message: 'There was an error initializing Payload',
|
||||||
stack: err.stack,
|
|
||||||
},
|
},
|
||||||
{ status: httpStatus.INTERNAL_SERVER_ERROR },
|
{ status: httpStatus.INTERNAL_SERVER_ERROR },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { config, logger } = req.payload
|
const { config, logger } = payload
|
||||||
|
|
||||||
let response = formatErrors(err)
|
let response = formatErrors(err)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { Collection, CustomPayloadRequest, SanitizedConfig } from 'payload/types'
|
import type { Collection, CustomPayloadRequest, SanitizedConfig } from 'payload/types'
|
||||||
|
|
||||||
|
import type { NextFileUploadOptions } from '../next-fileupload/index.js'
|
||||||
|
|
||||||
import { nextFileUpload } from '../next-fileupload/index.js'
|
import { nextFileUpload } from '../next-fileupload/index.js'
|
||||||
|
|
||||||
type GetDataAndFile = (args: {
|
type GetDataAndFile = (args: {
|
||||||
@@ -10,48 +12,40 @@ type GetDataAndFile = (args: {
|
|||||||
data: Record<string, any>
|
data: Record<string, any>
|
||||||
file: CustomPayloadRequest['file']
|
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 data: Record<string, any> = undefined
|
||||||
let file: CustomPayloadRequest['file'] = 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(';')
|
const [contentType] = (request.headers.get('Content-Type') || '').split(';')
|
||||||
|
|
||||||
if (contentType === 'application/json') {
|
if (contentType === 'application/json') {
|
||||||
|
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 {
|
try {
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
data = {}
|
data = {}
|
||||||
}
|
}
|
||||||
} else if (contentType === 'multipart/form-data') {
|
} else {
|
||||||
// possible upload request
|
throw new Error('Request body size exceeds the limit')
|
||||||
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 {
|
} else {
|
||||||
// store temp file on disk
|
if (request.headers.has('Content-Length') && request.headers.get('Content-Length') !== '0') {
|
||||||
const { error, fields, files } = await nextFileUpload({
|
const { error, fields, files } = await nextFileUpload({
|
||||||
options: config.upload as any,
|
options: config.upload as NextFileUploadOptions,
|
||||||
request,
|
request,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -59,21 +53,14 @@ export const getDataAndFile: GetDataAndFile = async ({ collection, config, reque
|
|||||||
throw new Error(error.message)
|
throw new Error(error.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (files?.file) file = files.file
|
if (collection?.config?.upload && files?.file) {
|
||||||
|
file = files.file
|
||||||
|
}
|
||||||
|
|
||||||
if (fields?._payload && typeof fields._payload === 'string') {
|
if (fields?._payload && typeof fields._payload === 'string') {
|
||||||
data = JSON.parse(fields._payload)
|
data = JSON.parse(fields._payload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// non upload request
|
|
||||||
const formData = await request.formData()
|
|
||||||
const payloadData = formData.get('_payload')
|
|
||||||
|
|
||||||
if (typeof payloadData === 'string') {
|
|
||||||
data = JSON.parse(payloadData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,20 @@ import type { SanitizedConfig } from 'payload/config'
|
|||||||
import type { Where } from 'payload/types'
|
import type { Where } from 'payload/types'
|
||||||
import type { ParsedQs } from 'qs'
|
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 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'
|
import { devUser } from '../credentials.js'
|
||||||
|
|
||||||
type ValidPath = `/${string}`
|
type ValidPath = `/${string}`
|
||||||
type RequestOptions = {
|
type RequestOptions = {
|
||||||
auth?: boolean
|
auth?: boolean
|
||||||
file?: boolean
|
|
||||||
query?: {
|
query?: {
|
||||||
depth?: number
|
depth?: number
|
||||||
fallbackLocale?: string
|
fallbackLocale?: string
|
||||||
@@ -28,6 +27,10 @@ type RequestOptions = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FileArg = {
|
||||||
|
file?: Omit<File, 'webkitRelativePath'>
|
||||||
|
}
|
||||||
|
|
||||||
function generateQueryString(query: RequestOptions['query'], params: ParsedQs): string {
|
function generateQueryString(query: RequestOptions['query'], params: ParsedQs): string {
|
||||||
return QueryString.stringify(
|
return QueryString.stringify(
|
||||||
{
|
{
|
||||||
@@ -67,12 +70,16 @@ export class NextRESTClient {
|
|||||||
this._GRAPHQL_POST = createGraphqlPOST(config)
|
this._GRAPHQL_POST = createGraphqlPOST(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildHeaders(options: RequestInit & RequestOptions): Headers {
|
private buildHeaders(options: RequestInit & RequestOptions & FileArg): Headers {
|
||||||
const defaultHeaders = {
|
const defaultHeaders = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
}
|
}
|
||||||
const headers = new Headers({
|
const headers = new Headers({
|
||||||
...(options?.file ? {} : defaultHeaders),
|
...(options?.file
|
||||||
|
? {
|
||||||
|
'Content-Length': options.file.size.toString(),
|
||||||
|
}
|
||||||
|
: defaultHeaders),
|
||||||
...(options?.headers || {}),
|
...(options?.headers || {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -141,7 +148,7 @@ export class NextRESTClient {
|
|||||||
return this._GRAPHQL_POST(request)
|
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 { url, slug, params } = this.generateRequestParts(path)
|
||||||
const { query, ...rest } = options
|
const { query, ...rest } = options
|
||||||
const queryParams = generateQueryString(query, params)
|
const queryParams = generateQueryString(query, params)
|
||||||
@@ -156,11 +163,10 @@ export class NextRESTClient {
|
|||||||
|
|
||||||
async POST(
|
async POST(
|
||||||
path: ValidPath,
|
path: ValidPath,
|
||||||
options: RequestInit & RequestOptions & { file?: boolean } = {},
|
options: RequestInit & RequestOptions & FileArg = {},
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const { url, slug, params } = this.generateRequestParts(path)
|
const { url, slug, params } = this.generateRequestParts(path)
|
||||||
const queryParams = generateQueryString({}, params)
|
const queryParams = generateQueryString({}, params)
|
||||||
|
|
||||||
const request = new Request(`${url}${queryParams}`, {
|
const request = new Request(`${url}${queryParams}`, {
|
||||||
...options,
|
...options,
|
||||||
method: 'POST',
|
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) => {
|
onInit: async (payload) => {
|
||||||
const uploadsDir = path.resolve(dirname, './media')
|
const uploadsDir = path.resolve(dirname, './media')
|
||||||
removeFiles(path.normalize(uploadsDir))
|
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 () => {
|
test('Should render adminThumbnail when using a function', async () => {
|
||||||
await page.goto(adminThumbnailFunctionURL.list)
|
await page.goto(adminThumbnailFunctionURL.list)
|
||||||
await page.waitForURL(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 type { Payload } from 'payload'
|
||||||
|
|
||||||
import { File as FileBuffer } from 'buffer'
|
|
||||||
import NodeFormData from 'form-data'
|
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { getFileByPath } from 'payload/uploads'
|
import { getFileByPath } from 'payload/uploads'
|
||||||
@@ -13,6 +11,7 @@ import type { Enlarge, Media } from './payload-types.js'
|
|||||||
|
|
||||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||||
import configPromise from './config.js'
|
import configPromise from './config.js'
|
||||||
|
import { createStreamableFile } from './createStreamableFile.js'
|
||||||
import {
|
import {
|
||||||
enlargeSlug,
|
enlargeSlug,
|
||||||
mediaSlug,
|
mediaSlug,
|
||||||
@@ -24,54 +23,6 @@ import {
|
|||||||
|
|
||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
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)
|
const stat = promisify(fs.stat)
|
||||||
|
|
||||||
let restClient: NextRESTClient
|
let restClient: NextRESTClient
|
||||||
@@ -90,20 +41,22 @@ describe('Collections - Uploads', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('REST', () => {
|
describe('REST API', () => {
|
||||||
describe('create', () => {
|
describe('create', () => {
|
||||||
it('creates from form data given a png', async () => {
|
it('creates from form data given a png', async () => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
const filePath = path.join(dirname, './image.png')
|
const filePath = path.join(dirname, './image.png')
|
||||||
|
const { file, handle } = await createStreamableFile(filePath)
|
||||||
formData.append('file', await bufferToFileBlob(filePath))
|
formData.append('file', file)
|
||||||
|
|
||||||
const response = await restClient.POST(`/${mediaSlug}`, {
|
const response = await restClient.POST(`/${mediaSlug}`, {
|
||||||
body: formData,
|
body: formData,
|
||||||
file: true,
|
file,
|
||||||
})
|
})
|
||||||
const { doc } = await response.json()
|
const { doc } = await response.json()
|
||||||
|
|
||||||
|
await handle.close()
|
||||||
|
|
||||||
expect(response.status).toBe(201)
|
expect(response.status).toBe(201)
|
||||||
|
|
||||||
const { sizes } = doc
|
const { sizes } = doc
|
||||||
@@ -130,16 +83,20 @@ describe('Collections - Uploads', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('creates from form data given an svg', async () => {
|
it('creates from form data given an svg', async () => {
|
||||||
const formData = new FormData()
|
|
||||||
const filePath = path.join(dirname, './image.svg')
|
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}`, {
|
const response = await restClient.POST(`/${mediaSlug}`, {
|
||||||
body: formData,
|
body: formData,
|
||||||
file: true,
|
file,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { doc } = await response.json()
|
const { doc } = await response.json()
|
||||||
|
|
||||||
|
await handle.close()
|
||||||
|
|
||||||
expect(response.status).toBe(201)
|
expect(response.status).toBe(201)
|
||||||
|
|
||||||
// Check for files
|
// Check for files
|
||||||
@@ -151,19 +108,21 @@ describe('Collections - Uploads', () => {
|
|||||||
expect(doc.width).toBeDefined()
|
expect(doc.width).toBeDefined()
|
||||||
expect(doc.height).toBeDefined()
|
expect(doc.height).toBeDefined()
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
it('should have valid image url', async () => {
|
it('should have valid image url', async () => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
const fileBlob = await bufferToFileBlob(path.join(dirname, './image.svg'))
|
const filePath = path.join(dirname, './image.svg')
|
||||||
formData.append('file', fileBlob)
|
const { file, handle } = await createStreamableFile(filePath)
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
const response = await restClient.POST(`/${mediaSlug}`, {
|
const response = await restClient.POST(`/${mediaSlug}`, {
|
||||||
body: formData,
|
body: formData,
|
||||||
file: true,
|
file,
|
||||||
})
|
})
|
||||||
const { doc } = await response.json()
|
const { doc } = await response.json()
|
||||||
|
|
||||||
|
await handle.close()
|
||||||
|
|
||||||
expect(response.status).toBe(201)
|
expect(response.status).toBe(201)
|
||||||
const expectedPath = path.join(dirname, './media')
|
const expectedPath = path.join(dirname, './media')
|
||||||
expect(await fileExists(path.join(expectedPath, doc.filename))).toBe(true)
|
expect(await fileExists(path.join(expectedPath, doc.filename))).toBe(true)
|
||||||
@@ -173,15 +132,18 @@ describe('Collections - Uploads', () => {
|
|||||||
|
|
||||||
it('creates images that do not require all sizes', async () => {
|
it('creates images that do not require all sizes', async () => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
const fileBlob = await bufferToFileBlob(path.join(dirname, './small.png'))
|
const filePath = path.join(dirname, './small.png')
|
||||||
formData.append('file', fileBlob)
|
const { file, handle } = await createStreamableFile(filePath)
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
const response = await restClient.POST(`/${mediaSlug}`, {
|
const response = await restClient.POST(`/${mediaSlug}`, {
|
||||||
body: formData,
|
body: formData,
|
||||||
file: true,
|
file,
|
||||||
})
|
})
|
||||||
const { doc } = await response.json()
|
const { doc } = await response.json()
|
||||||
|
|
||||||
|
await handle.close()
|
||||||
|
|
||||||
expect(response.status).toBe(201)
|
expect(response.status).toBe(201)
|
||||||
|
|
||||||
const expectedPath = path.join(dirname, './media')
|
const expectedPath = path.join(dirname, './media')
|
||||||
@@ -198,15 +160,18 @@ describe('Collections - Uploads', () => {
|
|||||||
|
|
||||||
it('creates images from a different format', async () => {
|
it('creates images from a different format', async () => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
const fileBlob = await bufferToFileBlob(path.join(dirname, './image.jpg'))
|
const filePath = path.join(dirname, './image.jpg')
|
||||||
formData.append('file', fileBlob)
|
const { file, handle } = await createStreamableFile(filePath)
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
const response = await restClient.POST(`/${mediaSlug}`, {
|
const response = await restClient.POST(`/${mediaSlug}`, {
|
||||||
body: formData,
|
body: formData,
|
||||||
file: true,
|
file,
|
||||||
})
|
})
|
||||||
const { doc } = await response.json()
|
const { doc } = await response.json()
|
||||||
|
|
||||||
|
await handle.close()
|
||||||
|
|
||||||
expect(response.status).toBe(201)
|
expect(response.status).toBe(201)
|
||||||
|
|
||||||
const expectedPath = path.join(dirname, './media')
|
const expectedPath = path.join(dirname, './media')
|
||||||
@@ -226,16 +191,19 @@ describe('Collections - Uploads', () => {
|
|||||||
|
|
||||||
it('creates media without storing a file', async () => {
|
it('creates media without storing a file', async () => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
const fileBlob = await bufferToFileBlob(path.join(dirname, './unstored.png'))
|
const filePath = path.join(dirname, './unstored.png')
|
||||||
formData.append('file', fileBlob)
|
const { file, handle } = await createStreamableFile(filePath)
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
// unstored media
|
// unstored media
|
||||||
const response = await restClient.POST(`/${unstoredMediaSlug}`, {
|
const response = await restClient.POST(`/${unstoredMediaSlug}`, {
|
||||||
body: formData,
|
body: formData,
|
||||||
file: true,
|
file,
|
||||||
})
|
})
|
||||||
const { doc } = await response.json()
|
const { doc } = await response.json()
|
||||||
|
|
||||||
|
await handle.close()
|
||||||
|
|
||||||
expect(response.status).toBe(201)
|
expect(response.status).toBe(201)
|
||||||
|
|
||||||
// Check for files
|
// Check for files
|
||||||
@@ -244,7 +212,322 @@ describe('Collections - Uploads', () => {
|
|||||||
// Check api response
|
// Check api response
|
||||||
expect(doc.filename).toBeDefined()
|
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 () => {
|
it('should enlarge images if resize options `withoutEnlargement` is set to false', async () => {
|
||||||
const small = await getFileByPath(path.resolve(dirname, './small.png'))
|
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 () => {
|
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 small = await getFileByPath(path.resolve(dirname, './small.png'))
|
||||||
|
|
||||||
const result = await payload.create({
|
const result = await payload.create({
|
||||||
@@ -359,294 +640,7 @@ describe('Collections - Uploads', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('update', async () => {
|
describe('Required Files', () => {
|
||||||
// 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', () => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/require-await
|
// eslint-disable-next-line @typescript-eslint/require-await
|
||||||
it('should allow file to be optional if filesRequiredOnCreate is false', async () => {
|
it('should allow file to be optional if filesRequiredOnCreate is false', async () => {
|
||||||
const successfulCreate = await payload.create({
|
const successfulCreate = await payload.create({
|
||||||
|
|||||||
Reference in New Issue
Block a user