Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
373 lines
15 KiB
Plaintext
373 lines
15 KiB
Plaintext
import { fileTypeFromBuffer } from 'file-type';
|
|
import fs from 'fs/promises';
|
|
import sanitize from 'sanitize-filename';
|
|
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 { createImageSizes } from './image-resizing/createImageSizes.js';
|
|
import { isImage } from './isImage.js';
|
|
import { optionallyAppendMetadata } from './optionallyAppendMetadata.js';
|
|
const shouldReupload = (uploadEdits, fileData)=>{
|
|
if (!fileData) {
|
|
return false;
|
|
}
|
|
if (uploadEdits.crop || uploadEdits.heightInPixels || uploadEdits.widthInPixels) {
|
|
return true;
|
|
}
|
|
// Since uploadEdits always has focalPoint, compare to the value in the data if it was changed
|
|
if (uploadEdits.focalPoint) {
|
|
const incomingFocalX = uploadEdits.focalPoint.x;
|
|
const incomingFocalY = uploadEdits.focalPoint.y;
|
|
const currentFocalX = 'focalX' in fileData && fileData.focalX;
|
|
const currentFocalY = 'focalY' in fileData && fileData.focalY;
|
|
const isEqual = incomingFocalX === currentFocalX && incomingFocalY === currentFocalY;
|
|
return !isEqual;
|
|
}
|
|
return false;
|
|
};
|
|
export const generateFileData = async ({ collection: { config: collectionConfig }, data, isDuplicating, operation, originalDoc, overwriteExistingFiles, req, throwOnMissingFile })=>{
|
|
if (!collectionConfig.upload) {
|
|
return {
|
|
data,
|
|
files: []
|
|
};
|
|
}
|
|
const { serverURL, sharp } = req.payload.config;
|
|
let file = req.file;
|
|
const uploadEdits = parseUploadEditsFromReqOrIncomingData({
|
|
data,
|
|
isDuplicating,
|
|
operation,
|
|
originalDoc,
|
|
req
|
|
});
|
|
const { constructorOptions, disableLocalStorage, focalPoint: focalPointEnabled = true, formatOptions, imageSizes, resizeOptions, staticDir, trimOptions, withMetadata } = collectionConfig.upload;
|
|
const staticPath = staticDir;
|
|
const incomingFileData = isDuplicating ? originalDoc : data;
|
|
let isLocalFile = false;
|
|
if (!file && (isDuplicating || shouldReupload(uploadEdits, incomingFileData))) {
|
|
const { filename, url } = incomingFileData;
|
|
if (filename && (filename.includes('../') || filename.includes('..\\'))) {
|
|
throw new Forbidden(req.t);
|
|
}
|
|
if (serverURL && url?.startsWith(serverURL) || url?.startsWith('/')) {
|
|
isLocalFile = true;
|
|
}
|
|
try {
|
|
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,
|
|
req,
|
|
uploadConfig: collectionConfig.upload
|
|
});
|
|
overwriteExistingFiles = true;
|
|
}
|
|
} catch (err) {
|
|
throw new FileRetrievalError(req.t, err instanceof Error ? err.message : undefined);
|
|
}
|
|
}
|
|
if (isDuplicating) {
|
|
overwriteExistingFiles = false;
|
|
}
|
|
if (!file) {
|
|
if (throwOnMissingFile) {
|
|
throw new MissingFile(req.t);
|
|
}
|
|
return {
|
|
data: incomingFileData,
|
|
files: []
|
|
};
|
|
}
|
|
await checkFileRestrictions({
|
|
collection: collectionConfig,
|
|
file,
|
|
req
|
|
});
|
|
if (!disableLocalStorage) {
|
|
await fs.mkdir(staticPath, {
|
|
recursive: true
|
|
});
|
|
}
|
|
let newData = incomingFileData;
|
|
const filesToSave = [];
|
|
const fileData = {};
|
|
const fileIsAnimatedType = [
|
|
'image/avif',
|
|
'image/gif',
|
|
'image/webp'
|
|
].includes(file.mimetype);
|
|
const cropData = typeof uploadEdits === 'object' && 'crop' in uploadEdits ? uploadEdits.crop : undefined;
|
|
try {
|
|
const fileSupportsResize = canResizeImage(file.mimetype);
|
|
let fsSafeName;
|
|
let sharpFile;
|
|
let dimensions;
|
|
let fileBuffer;
|
|
let ext;
|
|
let mime;
|
|
const fileHasAdjustments = fileSupportsResize && Boolean(resizeOptions || formatOptions || trimOptions || constructorOptions || file.tempFilePath);
|
|
const sharpOptions = {
|
|
...constructorOptions
|
|
};
|
|
if (fileIsAnimatedType) {
|
|
sharpOptions.animated = true;
|
|
}
|
|
if (sharp && (fileIsAnimatedType || fileHasAdjustments)) {
|
|
if (file.tempFilePath) {
|
|
sharpFile = sharp(file.tempFilePath, sharpOptions).rotate(); // pass rotate() to auto-rotate based on EXIF data. https://github.com/payloadcms/payload/pull/3081
|
|
} else {
|
|
sharpFile = sharp(file.data, sharpOptions).rotate(); // pass rotate() to auto-rotate based on EXIF data. https://github.com/payloadcms/payload/pull/3081
|
|
}
|
|
if (fileHasAdjustments) {
|
|
if (resizeOptions) {
|
|
sharpFile = sharpFile.resize(resizeOptions);
|
|
}
|
|
if (formatOptions) {
|
|
sharpFile = sharpFile.toFormat(formatOptions.format, formatOptions.options);
|
|
}
|
|
if (trimOptions) {
|
|
sharpFile = sharpFile.trim(trimOptions);
|
|
}
|
|
}
|
|
}
|
|
if (fileSupportsResize || isImage(file.mimetype)) {
|
|
dimensions = await getImageSize(file);
|
|
fileData.width = dimensions.width;
|
|
fileData.height = dimensions.height;
|
|
}
|
|
if (sharpFile) {
|
|
const metadata = await sharpFile.metadata();
|
|
sharpFile = await optionallyAppendMetadata({
|
|
req,
|
|
sharpFile,
|
|
withMetadata: withMetadata
|
|
});
|
|
fileBuffer = await sharpFile.toBuffer({
|
|
resolveWithObject: true
|
|
});
|
|
({ ext, mime } = await fileTypeFromBuffer(fileBuffer.data) // This is getting an incorrect gif height back.
|
|
);
|
|
fileData.width = fileBuffer.info.width;
|
|
fileData.height = fileBuffer.info.height;
|
|
fileData.filesize = fileBuffer.info.size;
|
|
// Animated GIFs + WebP aggregate the height from every frame, so we need to use divide by number of pages
|
|
if (metadata.pages) {
|
|
fileData.height = fileBuffer.info.height / metadata.pages;
|
|
fileData.filesize = fileBuffer.data.length;
|
|
}
|
|
} else {
|
|
mime = file.mimetype;
|
|
fileData.filesize = file.size;
|
|
if (file.name.includes('.')) {
|
|
ext = file.name.split('.').pop()?.split('?')[0];
|
|
} else {
|
|
ext = '';
|
|
}
|
|
}
|
|
// Adjust SVG mime type. fromBuffer modifies it.
|
|
if (mime === 'application/xml' && ext === 'svg') {
|
|
mime = 'image/svg+xml';
|
|
}
|
|
fileData.mimeType = mime;
|
|
const baseFilename = sanitize(file.name.substring(0, file.name.lastIndexOf('.')) || file.name);
|
|
fsSafeName = `${baseFilename}${ext ? `.${ext}` : ''}`;
|
|
if (!overwriteExistingFiles) {
|
|
// Extract prefix if present (added by plugin-cloud-storage)
|
|
const prefix = data?.prefix;
|
|
fsSafeName = await getSafeFileName({
|
|
collectionSlug: collectionConfig.slug,
|
|
desiredFilename: fsSafeName,
|
|
prefix,
|
|
req,
|
|
staticPath: staticPath
|
|
});
|
|
}
|
|
fileData.filename = fsSafeName;
|
|
let fileForResize = file;
|
|
if (cropData && sharp) {
|
|
const { data: croppedImage, info } = await cropImage({
|
|
cropData,
|
|
dimensions: dimensions,
|
|
file,
|
|
heightInPixels: uploadEdits.heightInPixels,
|
|
req,
|
|
sharp,
|
|
widthInPixels: uploadEdits.widthInPixels,
|
|
withMetadata
|
|
});
|
|
// Apply resize after cropping to ensure it conforms to resizeOptions
|
|
if (resizeOptions && !resizeOptions.withoutEnlargement) {
|
|
const resizedAfterCrop = await sharp(croppedImage).resize({
|
|
fit: resizeOptions?.fit || 'cover',
|
|
height: resizeOptions?.height,
|
|
position: resizeOptions?.position || 'center',
|
|
width: resizeOptions?.width
|
|
}).toBuffer({
|
|
resolveWithObject: true
|
|
});
|
|
filesToSave.push({
|
|
buffer: resizedAfterCrop.data,
|
|
path: `${staticPath}/${fsSafeName}`
|
|
});
|
|
fileForResize = {
|
|
...fileForResize,
|
|
data: resizedAfterCrop.data,
|
|
size: resizedAfterCrop.info.size
|
|
};
|
|
fileData.width = resizedAfterCrop.info.width;
|
|
fileData.height = resizedAfterCrop.info.height;
|
|
if (fileIsAnimatedType) {
|
|
const metadata = await sharpFile.metadata();
|
|
fileData.height = metadata.pages ? resizedAfterCrop.info.height / metadata.pages : resizedAfterCrop.info.height;
|
|
}
|
|
fileData.filesize = resizedAfterCrop.info.size;
|
|
} else {
|
|
// If resizeOptions is not present, just save the cropped image
|
|
filesToSave.push({
|
|
buffer: croppedImage,
|
|
path: `${staticPath}/${fsSafeName}`
|
|
});
|
|
fileForResize = {
|
|
...file,
|
|
data: croppedImage,
|
|
size: info.size
|
|
};
|
|
fileData.width = info.width;
|
|
fileData.height = info.height;
|
|
if (fileIsAnimatedType) {
|
|
const metadata = await sharpFile.metadata();
|
|
fileData.height = metadata.pages ? info.height / metadata.pages : info.height;
|
|
}
|
|
fileData.filesize = info.size;
|
|
}
|
|
if (file.tempFilePath) {
|
|
await fs.writeFile(file.tempFilePath, croppedImage); // write fileBuffer to the temp path
|
|
} else {
|
|
req.file = fileForResize;
|
|
}
|
|
} else {
|
|
// For non-image files with useTempFiles, read the buffer from the temp file
|
|
// since file.data is empty when using temp files
|
|
let bufferToSave;
|
|
if (fileBuffer?.data) {
|
|
bufferToSave = fileBuffer.data;
|
|
} else if (file.tempFilePath) {
|
|
bufferToSave = await fs.readFile(file.tempFilePath);
|
|
} else {
|
|
bufferToSave = file.data;
|
|
}
|
|
filesToSave.push({
|
|
buffer: bufferToSave,
|
|
path: `${staticPath}/${fsSafeName}`
|
|
});
|
|
// If using temp files and the image is being resized, write the file to the temp path
|
|
if (fileBuffer?.data || bufferToSave.length > 0) {
|
|
if (file.tempFilePath) {
|
|
await fs.writeFile(file.tempFilePath, fileBuffer?.data || bufferToSave); // write fileBuffer to the temp path
|
|
} else {
|
|
// Assign the _possibly modified_ file to the request object
|
|
req.file = {
|
|
...file,
|
|
data: fileBuffer?.data || bufferToSave,
|
|
size: fileBuffer?.info.size
|
|
};
|
|
}
|
|
}
|
|
}
|
|
if (fileSupportsResize && (Array.isArray(imageSizes) || focalPointEnabled !== false)) {
|
|
req.payloadUploadSizes = {};
|
|
// 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 : {
|
|
...dimensions,
|
|
height: fileData.height,
|
|
width: fileData.width
|
|
},
|
|
file: fileForResize,
|
|
focalPoint,
|
|
mimeType: fileData.mimeType,
|
|
req,
|
|
savedFilename: fsSafeName || file.name,
|
|
sharp,
|
|
staticPath: staticPath,
|
|
withMetadata
|
|
});
|
|
fileData.sizes = sizeData;
|
|
fileData.focalX = focalPoint?.x;
|
|
fileData.focalY = focalPoint?.y;
|
|
filesToSave.push(...sizesToSave);
|
|
}
|
|
} catch (err) {
|
|
req.payload.logger.error(err);
|
|
throw new FileUploadError(req.t);
|
|
}
|
|
newData = {
|
|
...newData,
|
|
...fileData
|
|
};
|
|
return {
|
|
data: newData,
|
|
files: filesToSave
|
|
};
|
|
};
|
|
/**
|
|
* Parse upload edits from req or incoming data
|
|
*/ function parseUploadEditsFromReqOrIncomingData(args) {
|
|
const { data, isDuplicating, operation, originalDoc, req } = args;
|
|
// Get intended focal point change from query string or incoming data
|
|
const uploadEdits = req.query?.uploadEdits && typeof req.query.uploadEdits === 'object' ? req.query.uploadEdits : {};
|
|
if (uploadEdits.focalPoint) {
|
|
return uploadEdits;
|
|
}
|
|
const incomingData = data;
|
|
const origDoc = originalDoc;
|
|
if (origDoc && 'focalX' in origDoc && 'focalY' in origDoc) {
|
|
// If no change in focal point, return undefined.
|
|
// This prevents a refocal operation triggered from admin, because it always sends the focal point.
|
|
if (incomingData.focalX === origDoc.focalX && incomingData.focalY === origDoc.focalY) {
|
|
return undefined;
|
|
}
|
|
if (isDuplicating) {
|
|
uploadEdits.focalPoint = {
|
|
x: incomingData?.focalX || origDoc.focalX,
|
|
y: incomingData?.focalY || origDoc.focalY
|
|
};
|
|
return uploadEdits;
|
|
}
|
|
}
|
|
if (incomingData?.focalX && incomingData?.focalY) {
|
|
uploadEdits.focalPoint = {
|
|
x: incomingData.focalX,
|
|
y: incomingData.focalY
|
|
};
|
|
return uploadEdits;
|
|
}
|
|
// If no focal point is set, default to center
|
|
if (operation === 'create') {
|
|
uploadEdits.focalPoint = {
|
|
x: 50,
|
|
y: 50
|
|
};
|
|
}
|
|
return uploadEdits;
|
|
}
|
|
|
|
//# sourceMappingURL=generateFileData.js.map |