Skip to content
40 changes: 29 additions & 11 deletions packages/payload/src/uploads/generateFileData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@ import sanitize from 'sanitize-filename'

import type { Collection } from '../collections/config/types.js'
import type { SanitizedConfig } from '../config/types.js'
import type { PayloadRequest } from '../types/index.js'
import type { Document, PayloadRequest } from '../types/index.js'
import type { FileData, FileToSave, ProbedImageSize, UploadEdits } from './types.js'

import { FileRetrievalError, FileUploadError, Forbidden, MissingFile } from '../errors/index.js'
import { isNumber } from '../utilities/isNumber.js'
import { canResizeImage } from './canResizeImage.js'
import { checkFileRestrictions } from './checkFileRestrictions.js'
import { cropImage } from './cropImage.js'
import { getExternalFile } from './getExternalFile.js'
import { getFileByPath } from './getFileByPath.js'
import { getImageSize } from './getImageSize.js'
import { getSafeFileName } from './getSafeFilename.js'
import { resizeAndTransformImageSizes } from './imageResizer.js'
import { createImageSizes } from './image-resizing/createImageSizes.js'
import { isImage } from './isImage.js'
import { optionallyAppendMetadata } from './optionallyAppendMetadata.js'
type Args<T> = {
Expand Down Expand Up @@ -81,7 +82,7 @@ export const generateFileData = async <T>({
}
}

const { sharp } = req.payload.config
const { serverURL, sharp } = req.payload.config

let file = req.file

Expand All @@ -107,25 +108,31 @@ export const generateFileData = async <T>({

const staticPath = staticDir

const incomingFileData = isDuplicating ? originalDoc : data
const incomingFileData: Document = isDuplicating ? originalDoc : data
let isLocalFile = false

if (
!file &&
(isDuplicating || shouldReupload(uploadEdits, incomingFileData as Record<string, unknown>))
) {
const { filename, url } = incomingFileData as unknown as FileData

if (filename && (filename.includes('../') || filename.includes('..\\'))) {
throw new Forbidden(req.t)
}

if ((serverURL && url?.startsWith(serverURL)) || url?.startsWith('/')) {
isLocalFile = true
}

try {
if (url && url.startsWith('/') && !disableLocalStorage) {
if (!disableLocalStorage && isLocalFile) {
// File is stored locally
const filePath = `${staticPath}/${filename}`
const response = await getFileByPath(filePath)
file = response
overwriteExistingFiles = true
} else if (filename && url) {
// File is remote
file = await getExternalFile({
data: incomingFileData as unknown as FileData,
req,
Expand All @@ -148,7 +155,7 @@ export const generateFileData = async <T>({
}

return {
data,
data: incomingFileData!,
files: [],
}
}
Expand All @@ -163,7 +170,7 @@ export const generateFileData = async <T>({
await fs.mkdir(staticPath!, { recursive: true })
}

let newData = data
let newData = incomingFileData as T
const filesToSave: FileToSave[] = []
const fileData: Partial<FileData> = {}
const fileIsAnimatedType = ['image/avif', 'image/gif', 'image/webp'].includes(file.mimetype)
Expand Down Expand Up @@ -267,6 +274,7 @@ export const generateFileData = async <T>({
}

fileData.filename = fsSafeName

let fileForResize = file

if (cropData && sharp) {
Expand Down Expand Up @@ -362,7 +370,16 @@ export const generateFileData = async <T>({

if (fileSupportsResize && (Array.isArray(imageSizes) || focalPointEnabled !== false)) {
req.payloadUploadSizes = {}
const { focalPoint, sizeData, sizesToSave } = await resizeAndTransformImageSizes({
// Focal point adjustments
const focalPoint =
focalPointEnabled && uploadEdits?.focalPoint
? {
x: isNumber(uploadEdits.focalPoint.x) ? Math.round(uploadEdits.focalPoint.x) : 50,
y: isNumber(uploadEdits.focalPoint.y) ? Math.round(uploadEdits.focalPoint.y) : 50,
}
: undefined

const { sizeData, sizesToSave } = await createImageSizes({
config: collectionConfig,
dimensions: !cropData
? dimensions!
Expand All @@ -372,12 +389,12 @@ export const generateFileData = async <T>({
width: fileData.width!,
},
file: fileForResize,
focalPoint,
mimeType: fileData.mimeType,
req,
savedFilename: fsSafeName || file.name,
sharp,
staticPath: staticPath!,
uploadEdits,
withMetadata,
})

Expand Down Expand Up @@ -437,8 +454,9 @@ function parseUploadEditsFromReqOrIncomingData(args: {
if (isDuplicating) {
uploadEdits.focalPoint = {
x: incomingData?.focalX || origDoc.focalX!,
y: incomingData?.focalY || origDoc.focalX!,
y: incomingData?.focalY || origDoc.focalY!,
}
return uploadEdits
}
}

Expand Down
47 changes: 47 additions & 0 deletions packages/payload/src/uploads/generateFilePathOrURL.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { Config } from '../config/types.js'

import { formatAdminURL } from '../utilities/formatAdminURL.js'

/**
* Generates a file path or URL based on the provided parameters.
*
* If urlOrPath is an external URL, it returns it as is.
* If a filename is provided, it constructs a URL using the collection slug and API route.
* If neither condition is met, it returns null.
*
* If you set relative to true, the returned URL will be relative to the serverURL (unless external).
*/
export function generateFilePathOrURL({
collectionSlug,
config,
filename,
relative,
serverURL,
urlOrPath,
}: {
collectionSlug: string
config: Config
filename?: string
relative: boolean
serverURL?: string
urlOrPath: string | undefined
}): null | string {
if (urlOrPath) {
if (!urlOrPath.startsWith('/') && !urlOrPath.startsWith(serverURL || '')) {
// external url
return urlOrPath
}
}

if (filename) {
// local file url
return formatAdminURL({
apiRoute: config.routes?.api || '',
path: `/${collectionSlug}/file/${encodeURIComponent(filename)}`,
relative,
serverURL: config.serverURL,
})
}

return null
}
121 changes: 64 additions & 57 deletions packages/payload/src/uploads/getBaseFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,9 @@ import type { Config } from '../config/types.js'
import type { Field } from '../fields/config/types.js'
import type { UploadConfig } from './types.js'

import { formatAdminURL } from '../utilities/formatAdminURL.js'
import { generateFilePathOrURL } from './generateFilePathOrURL.js'
import { mimeTypeValidator } from './mimeTypeValidator.js'

type GenerateURLArgs = {
collectionSlug: string
config: Config
filename?: string
}
const generateURL = ({ collectionSlug, config, filename }: GenerateURLArgs) => {
if (filename) {
return formatAdminURL({
apiRoute: config.routes?.api || '',
path: `/${collectionSlug}/file/${encodeURIComponent(filename)}`,
serverURL: config.serverURL,
})
}
return undefined
}

type Options = {
collection: CollectionConfig
config: Config
Expand Down Expand Up @@ -49,29 +33,38 @@ export const getBaseUploadFields = ({ collection, config }: Options): Field[] =>
},
hooks: {
afterRead: [
({ originalDoc }) => {
({ originalDoc, req, value }) => {
const adminThumbnail =
typeof collection.upload !== 'boolean' ? collection.upload?.adminThumbnail : undefined

if (typeof adminThumbnail === 'function') {
return adminThumbnail({ doc: originalDoc })
}

if (
typeof adminThumbnail === 'string' &&
'sizes' in originalDoc &&
originalDoc.sizes?.[adminThumbnail]?.filename
) {
return generateURL({
collectionSlug: collection.slug,
config,
filename: originalDoc.sizes?.[adminThumbnail].filename as string,
})
}

return null
return generateFilePathOrURL({
collectionSlug: collection.slug,
config,
filename:
typeof adminThumbnail === 'string'
? (originalDoc.sizes?.[adminThumbnail].filename as string)
: undefined,
relative: false,
serverURL: req.payload.config.serverURL,
urlOrPath: value,
})
},
],
beforeChange: [
({ collection, data, originalDoc, req, value }) =>
generateFilePathOrURL({
collectionSlug: collection?.slug as string,
config,
filename: data?.filename || originalDoc?.filename,
relative: true,
serverURL: req.payload.config.serverURL,
urlOrPath: value,
}),
],
},
label: 'Thumbnail URL',
}
Expand Down Expand Up @@ -141,17 +134,26 @@ export const getBaseUploadFields = ({ collection, config }: Options): Field[] =>
...url,
hooks: {
afterRead: [
({ data, value }) => {
if (value && !data?.filename) {
return value
}

return generateURL({
({ data, originalDoc, req, value }) =>
generateFilePathOrURL({
collectionSlug: collection.slug,
config,
filename: data?.filename,
})
},
filename: data?.filename || originalDoc?.filename,
relative: false,
serverURL: req.payload.config.serverURL,
urlOrPath: value,
}),
],
beforeChange: [
({ collection, data, originalDoc, req, value }) =>
generateFilePathOrURL({
collectionSlug: collection?.slug as string,
config,
filename: data?.filename || originalDoc?.filename,
relative: true,
serverURL: req.payload.config.serverURL,
urlOrPath: value,
}),
],
},
},
Expand Down Expand Up @@ -220,23 +222,28 @@ export const getBaseUploadFields = ({ collection, config }: Options): Field[] =>
},
hooks: {
afterRead: [
({ data, value }) => {
if (value && size.height && size.width && !data?.filename) {
return value
}

const sizeFilename = data?.sizes?.[size.name]?.filename

if (sizeFilename) {
return formatAdminURL({
apiRoute: config.routes?.api || '',
path: `/${collection.slug}/file/${encodeURIComponent(sizeFilename)}`,
serverURL: config.serverURL,
})
}

return null
},
({ collection, data, originalDoc, req, value }) =>
generateFilePathOrURL({
collectionSlug: collection?.slug as string,
config,
filename:
data?.sizes?.[size.name]?.filename ||
originalDoc?.sizes?.[size.name]?.filename,
relative: false,
serverURL: req.payload.config.serverURL,
urlOrPath: value,
}),
],
beforeChange: [
({ collection, data, originalDoc, req, value }) =>
generateFilePathOrURL({
collectionSlug: collection?.slug as string,
config,
filename: data?.filename || originalDoc?.filename,
relative: true,
serverURL: req.payload.config.serverURL,
urlOrPath: value,
}),
],
},
},
Expand Down
40 changes: 40 additions & 0 deletions packages/payload/src/uploads/image-resizing/createImageSize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Create the result object for the image resize operation based on the
* provided parameters. If the name is not provided, an empty result object
* is returned.
*
* @param filename - the filename of the image
* @param width - the width of the image
* @param height - the height of the image
* @param filesize - the filesize of the image
* @param mimeType - the mime type of the image
* @returns a FileSize result object
*/

import type { FileSize } from '../types.js'

type CreateResultArgs = {
filename?: FileSize['filename']
filesize?: FileSize['filesize']
height?: FileSize['height']
mimeType?: FileSize['mimeType']
url?: FileSize['url']
width?: FileSize['width']
}
export const createImageSize = ({
filename = null,
filesize = null,
height = null,
mimeType = null,
url = null,
width = null,
}: CreateResultArgs): FileSize => {
return {
filename,
filesize,
height,
mimeType,
url,
width,
}
}
Loading
Loading