payload
Version:
Node, React, Headless CMS and Application Framework built on Next.js
323 lines (322 loc) • 12.9 kB
JavaScript
// @ts-strict-ignore
import { fileTypeFromBuffer } from 'file-type';
import fs from 'fs/promises';
import sanitize from 'sanitize-filename';
import { FileRetrievalError, FileUploadError, MissingFile } from '../errors/index.js';
import { canResizeImage } from './canResizeImage.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 { isImage } from './isImage.js';
import { optionallyAppendMetadata } from './optionallyAppendMetadata.js';
export const generateFileData = async ({ collection: { config: collectionConfig }, data, isDuplicating, operation, originalDoc, overwriteExistingFiles, req, throwOnMissingFile })=>{
if (!collectionConfig.upload) {
return {
data,
files: []
};
}
const { sharp } = req.payload.config;
let file = req.file;
const uploadEdits = parseUploadEditsFromReqOrIncomingData({
data,
isDuplicating,
operation,
originalDoc,
req
});
const { disableLocalStorage, focalPoint: focalPointEnabled = true, formatOptions, imageSizes, resizeOptions, staticDir, trimOptions, withMetadata } = collectionConfig.upload;
const staticPath = staticDir;
const incomingFileData = isDuplicating ? originalDoc : data;
if (!file && uploadEdits && incomingFileData) {
const { filename, url } = incomingFileData;
try {
if (url && url.startsWith('/') && !disableLocalStorage) {
const filePath = `${staticPath}/${filename}`;
const response = await getFileByPath(filePath);
file = response;
overwriteExistingFiles = true;
} else if (filename && url) {
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,
files: []
};
}
if (!disableLocalStorage) {
await fs.mkdir(staticPath, {
recursive: true
});
}
let newData = data;
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 || file.tempFilePath);
const sharpOptions = {};
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
});
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) {
fsSafeName = await getSafeFileName({
collectionSlug: collectionConfig.slug,
desiredFilename: fsSafeName,
req,
staticPath
});
}
fileData.filename = fsSafeName;
let fileForResize = file;
if (cropData && sharp) {
const { data: croppedImage, info } = await cropImage({
cropData,
dimensions,
file,
heightInPixels: uploadEdits.heightInPixels,
req,
sharp,
widthInPixels: uploadEdits.widthInPixels,
withMetadata
});
// Apply resize after cropping to ensure it conforms to resizeOptions
if (resizeOptions) {
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 {
filesToSave.push({
buffer: fileBuffer?.data || file.data,
path: `${staticPath}/${fsSafeName}`
});
// If using temp files and the image is being resized, write the file to the temp path
if (fileBuffer?.data || file.data.length > 0) {
if (file.tempFilePath) {
await fs.writeFile(file.tempFilePath, fileBuffer?.data || file.data) // write fileBuffer to the temp path
;
} else {
// Assign the _possibly modified_ file to the request object
req.file = {
...file,
data: fileBuffer?.data || file.data,
size: fileBuffer?.info.size
};
}
}
}
if (fileSupportsResize && (Array.isArray(imageSizes) || focalPointEnabled !== false)) {
req.payloadUploadSizes = {};
const { focalPoint, sizeData, sizesToSave } = await resizeAndTransformImageSizes({
config: collectionConfig,
dimensions: !cropData ? dimensions : {
...dimensions,
height: fileData.height,
width: fileData.width
},
file: fileForResize,
mimeType: fileData.mimeType,
req,
savedFilename: fsSafeName || file.name,
sharp,
staticPath,
uploadEdits,
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.focalX
};
}
}
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