Feat/expose sharp options (#1029)
Co-authored-by: khakimvinh <kha.kim.vinh@gmail.com> Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
This commit is contained in:
@@ -135,11 +135,28 @@ const collectionSchema = joi.object().keys({
|
||||
width: joi.number().allow(null),
|
||||
height: joi.number().allow(null),
|
||||
crop: joi.string(), // TODO: add further specificity with joi.xor
|
||||
}),
|
||||
}).unknown(),
|
||||
),
|
||||
mimeTypes: joi.array().items(joi.string()),
|
||||
staticOptions: joi.object(),
|
||||
handlers: joi.array().items(joi.func()),
|
||||
resizeOptions: joi.object().keys({
|
||||
width: joi.number().allow(null),
|
||||
height: joi.number().allow(null),
|
||||
fit: joi.string(),
|
||||
position: joi.alternatives().try(
|
||||
joi.string(),
|
||||
joi.number(),
|
||||
),
|
||||
background: joi.string(),
|
||||
kernel: joi.string(),
|
||||
withoutEnlargement: joi.bool(),
|
||||
fastShrinkOnLoad: joi.bool(),
|
||||
}).allow(null),
|
||||
formatOptions: joi.object().keys({
|
||||
format: joi.string(),
|
||||
options: joi.object(),
|
||||
}),
|
||||
}),
|
||||
joi.boolean(),
|
||||
),
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Document } from '../../../types';
|
||||
import getFileByPath from '../../../uploads/getFileByPath';
|
||||
import create from '../create';
|
||||
import { getDataLoader } from '../../dataloader';
|
||||
import { File } from '../../../uploads/types';
|
||||
|
||||
|
||||
export type Options<T> = {
|
||||
@@ -18,7 +19,7 @@ export type Options<T> = {
|
||||
disableVerificationEmail?: boolean
|
||||
showHiddenFields?: boolean
|
||||
filePath?: string
|
||||
file?: UploadedFile
|
||||
file?: File
|
||||
overwriteExistingFiles?: boolean
|
||||
req?: PayloadRequest
|
||||
draft?: boolean
|
||||
@@ -49,7 +50,7 @@ export default async function createLocal<T = any>(payload: Payload, options: Op
|
||||
req.fallbackLocale = fallbackLocale || req?.fallbackLocale || null;
|
||||
req.payload = payload;
|
||||
req.files = {
|
||||
file: (file as UploadedFile) ?? (getFileByPath(filePath) as UploadedFile),
|
||||
file: (file ?? (await getFileByPath(filePath))) as UploadedFile,
|
||||
};
|
||||
|
||||
if (typeof user !== 'undefined') req.user = user;
|
||||
|
||||
@@ -49,7 +49,7 @@ export default async function updateLocal<T = any>(payload: Payload, options: Op
|
||||
fallbackLocale,
|
||||
payload,
|
||||
files: {
|
||||
file: file ?? getFileByPath(filePath),
|
||||
file: file ?? await getFileByPath(filePath),
|
||||
},
|
||||
} as PayloadRequest;
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import fs from 'fs';
|
||||
import mime from 'mime';
|
||||
import { fromFile } from 'file-type';
|
||||
import path from 'path';
|
||||
import { File } from './types';
|
||||
|
||||
const getFileByPath = (filePath: string): File => {
|
||||
const getFileByPath = async (filePath: string): Promise<File> => {
|
||||
if (typeof filePath === 'string') {
|
||||
const data = fs.readFileSync(filePath);
|
||||
const mimetype = mime.getType(filePath);
|
||||
const mimetype = fromFile(filePath);
|
||||
const { size } = fs.statSync(filePath);
|
||||
|
||||
const name = path.basename(filePath);
|
||||
|
||||
return {
|
||||
data,
|
||||
mimetype,
|
||||
mimetype: (await mimetype).mime,
|
||||
name,
|
||||
size,
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import fileExists from './fileExists';
|
||||
|
||||
const incrementName = (name: string) => {
|
||||
const extension = name.split('.').pop();
|
||||
const baseFilename = sanitize(name.substr(0, name.lastIndexOf('.')) || name);
|
||||
const baseFilename = sanitize(name.substring(0, name.lastIndexOf('.')) || name);
|
||||
let incrementedName = baseFilename;
|
||||
const regex = /(.*)-(\d)$/;
|
||||
const found = baseFilename.match(regex);
|
||||
@@ -15,8 +15,7 @@ const incrementName = (name: string) => {
|
||||
const matchedName = found[1];
|
||||
const matchedNumber = found[2];
|
||||
const incremented = Number(matchedNumber) + 1;
|
||||
const newName = `${matchedName}-${incremented}`;
|
||||
incrementedName = newName;
|
||||
incrementedName = `${matchedName}-${incremented}`;
|
||||
}
|
||||
return `${incrementedName}.${extension}`;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from 'fs';
|
||||
import sharp from 'sharp';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { fromBuffer } from 'file-type';
|
||||
import { ProbedImageSize } from './getImageSize';
|
||||
import fileExists from './fileExists';
|
||||
import { SanitizedCollectionConfig } from '../collections/config/types';
|
||||
@@ -8,18 +9,25 @@ import { FileSizes, ImageSize } from './types';
|
||||
import { PayloadRequest } from '../express/types';
|
||||
|
||||
type Args = {
|
||||
req: PayloadRequest,
|
||||
file: Buffer,
|
||||
dimensions: ProbedImageSize,
|
||||
staticPath: string,
|
||||
config: SanitizedCollectionConfig,
|
||||
savedFilename: string,
|
||||
mimeType: string,
|
||||
req: PayloadRequest
|
||||
file: Buffer
|
||||
dimensions: ProbedImageSize
|
||||
staticPath: string
|
||||
config: SanitizedCollectionConfig
|
||||
savedFilename: string
|
||||
mimeType: string
|
||||
}
|
||||
|
||||
function getOutputImage(sourceImage: string, size: ImageSize) {
|
||||
type OutputImage = {
|
||||
name: string,
|
||||
extension: string,
|
||||
width: number,
|
||||
height: number
|
||||
}
|
||||
|
||||
function getOutputImage(sourceImage: string, size: ImageSize): OutputImage {
|
||||
const extension = sourceImage.split('.').pop();
|
||||
const name = sanitize(sourceImage.substr(0, sourceImage.lastIndexOf('.')) || sourceImage);
|
||||
const name = sanitize(sourceImage.substring(0, sourceImage.lastIndexOf('.')) || sourceImage);
|
||||
|
||||
return {
|
||||
name,
|
||||
@@ -29,14 +37,6 @@ function getOutputImage(sourceImage: string, size: ImageSize) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description
|
||||
* @param staticPath Path to save images
|
||||
* @param config Payload config
|
||||
* @param savedFilename
|
||||
* @param mimeType
|
||||
* @returns image sizes keyed to strings
|
||||
*/
|
||||
export default async function resizeAndSave({
|
||||
req,
|
||||
file,
|
||||
@@ -44,17 +44,17 @@ export default async function resizeAndSave({
|
||||
staticPath,
|
||||
config,
|
||||
savedFilename,
|
||||
mimeType,
|
||||
}: Args): Promise<FileSizes> {
|
||||
const { imageSizes, disableLocalStorage } = config.upload;
|
||||
|
||||
const sizes = imageSizes
|
||||
.filter((desiredSize) => desiredSize.width <= dimensions.width || desiredSize.height <= dimensions.height)
|
||||
.filter((desiredSize) => needsResize(desiredSize, dimensions))
|
||||
.map(async (desiredSize) => {
|
||||
const resized = await sharp(file)
|
||||
.resize(desiredSize.width, desiredSize.height, {
|
||||
position: desiredSize.crop || 'centre',
|
||||
});
|
||||
let resized = await sharp(file).resize(desiredSize);
|
||||
|
||||
if (desiredSize.formatOptions) {
|
||||
resized = resized.toFormat(desiredSize.formatOptions.format, desiredSize.formatOptions.options);
|
||||
}
|
||||
|
||||
const bufferObject = await resized.toBuffer({
|
||||
resolveWithObject: true,
|
||||
@@ -63,7 +63,7 @@ export default async function resizeAndSave({
|
||||
req.payloadUploadSizes[desiredSize.name] = bufferObject.data;
|
||||
|
||||
const outputImage = getOutputImage(savedFilename, desiredSize);
|
||||
const imageNameWithDimensions = `${outputImage.name}-${bufferObject.info.width}x${bufferObject.info.height}.${outputImage.extension}`;
|
||||
const imageNameWithDimensions = createImageName(outputImage, bufferObject);
|
||||
const imagePath = `${staticPath}/${imageNameWithDimensions}`;
|
||||
const fileAlreadyExists = await fileExists(imagePath);
|
||||
|
||||
@@ -81,20 +81,34 @@ export default async function resizeAndSave({
|
||||
height: bufferObject.info.height,
|
||||
filename: imageNameWithDimensions,
|
||||
filesize: bufferObject.info.size,
|
||||
mimeType,
|
||||
mimeType: (await fromBuffer(bufferObject.data)).mime,
|
||||
};
|
||||
});
|
||||
|
||||
const savedSizes = await Promise.all(sizes);
|
||||
|
||||
return savedSizes.reduce((results, size) => ({
|
||||
...results,
|
||||
[size.name]: {
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
filename: size.filename,
|
||||
mimeType: size.mimeType,
|
||||
filesize: size.filesize,
|
||||
},
|
||||
}), {});
|
||||
return savedSizes.reduce(
|
||||
(results, size) => ({
|
||||
...results,
|
||||
[size.name]: {
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
filename: size.filename,
|
||||
mimeType: size.mimeType,
|
||||
filesize: size.filesize,
|
||||
},
|
||||
}),
|
||||
{},
|
||||
);
|
||||
}
|
||||
function createImageName(
|
||||
outputImage: OutputImage,
|
||||
bufferObject: { data: Buffer; info: sharp.OutputInfo },
|
||||
): string {
|
||||
return `${outputImage.name}-${bufferObject.info.width}x${bufferObject.info.height}.${outputImage.extension}`;
|
||||
}
|
||||
|
||||
function needsResize(desiredSize: ImageSize, dimensions: ProbedImageSize): boolean {
|
||||
return (typeof desiredSize.width === 'number' && desiredSize.width <= dimensions.width)
|
||||
|| (typeof desiredSize.height === 'number' && desiredSize.height <= dimensions.height);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import express from 'express';
|
||||
import serveStatic from 'serve-static';
|
||||
import { Sharp, ResizeOptions } from 'sharp';
|
||||
|
||||
export type FileSize = {
|
||||
filename: string;
|
||||
@@ -25,36 +26,50 @@ export type FileData = {
|
||||
sizes: FileSizes;
|
||||
};
|
||||
|
||||
export type ImageSize = {
|
||||
name: string,
|
||||
width: number | null,
|
||||
height: number | null,
|
||||
crop?: string, // comes from sharp package
|
||||
/**
|
||||
* Params sent to the sharp toFormat() function
|
||||
* @link https://sharp.pixelplumbing.com/api-output#toformat
|
||||
*/
|
||||
export type ImageUploadFormatOptions = {
|
||||
format: Parameters<Sharp['toFormat']>[0]
|
||||
options?: Parameters<Sharp['toFormat']>[1]
|
||||
}
|
||||
|
||||
export type ImageSize = ResizeOptions & {
|
||||
name: string
|
||||
formatOptions?: ImageUploadFormatOptions
|
||||
/**
|
||||
* @deprecated prefer position
|
||||
*/
|
||||
crop?: string // comes from sharp package
|
||||
};
|
||||
|
||||
export type GetAdminThumbnail = (args: { doc: Record<string, unknown> }) => string
|
||||
|
||||
export type IncomingUploadType = {
|
||||
imageSizes?: ImageSize[];
|
||||
staticURL?: string;
|
||||
staticDir?: string;
|
||||
imageSizes?: ImageSize[]
|
||||
staticURL?: string
|
||||
staticDir?: string
|
||||
disableLocalStorage?: boolean
|
||||
adminThumbnail?: string | GetAdminThumbnail;
|
||||
mimeTypes?: string[];
|
||||
adminThumbnail?: string | GetAdminThumbnail
|
||||
mimeTypes?: string[]
|
||||
staticOptions?: serveStatic.ServeStaticOptions<express.Response<any, Record<string, any>>>
|
||||
handlers?: any[]
|
||||
resizeOptions?: ResizeOptions
|
||||
formatOptions?: ImageUploadFormatOptions
|
||||
}
|
||||
|
||||
|
||||
export type Upload = {
|
||||
imageSizes?: ImageSize[]
|
||||
staticURL: string
|
||||
staticDir: string
|
||||
disableLocalStorage: boolean
|
||||
adminThumbnail?: string | GetAdminThumbnail
|
||||
mimeTypes?: string[];
|
||||
mimeTypes?: string[]
|
||||
staticOptions?: serveStatic.ServeStaticOptions<express.Response<any, Record<string, any>>>
|
||||
handlers?: any[]
|
||||
resizeOptions?: ResizeOptions;
|
||||
formatOptions?: ImageUploadFormatOptions
|
||||
}
|
||||
|
||||
export type File = {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import mkdirp from 'mkdirp';
|
||||
import path from 'path';
|
||||
import mime from 'mime';
|
||||
import sharp, { Sharp } from 'sharp';
|
||||
import { fromBuffer } from 'file-type';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { SanitizedConfig } from '../config/types';
|
||||
import { Collection } from '../collections/config/types';
|
||||
import { FileUploadError, MissingFile } from '../errors';
|
||||
@@ -37,7 +39,7 @@ const uploadFile = async ({
|
||||
if (collectionConfig.upload) {
|
||||
const fileData: Partial<FileData> = {};
|
||||
|
||||
const { staticDir, imageSizes, disableLocalStorage } = collectionConfig.upload;
|
||||
const { staticDir, imageSizes, disableLocalStorage, resizeOptions, formatOptions } = collectionConfig.upload;
|
||||
|
||||
const { file } = req.files || {};
|
||||
|
||||
@@ -56,16 +58,37 @@ const uploadFile = async ({
|
||||
}
|
||||
|
||||
if (file) {
|
||||
const fsSafeName = !overwriteExistingFiles ? await getSafeFileName(Model, staticPath, file.name) : file.name;
|
||||
|
||||
try {
|
||||
let fsSafeName: string;
|
||||
let fileBuffer: Buffer;
|
||||
let mimeType: string;
|
||||
let fileSize: number;
|
||||
|
||||
if (!disableLocalStorage) {
|
||||
await saveBufferToFile(file.data, `${staticPath}/${fsSafeName}`);
|
||||
let resized: Sharp | undefined;
|
||||
if (resizeOptions) {
|
||||
resized = sharp(file.data).resize(resizeOptions);
|
||||
}
|
||||
if (formatOptions) {
|
||||
resized = (resized ?? sharp(file.data)).toFormat(formatOptions.format, formatOptions.options);
|
||||
}
|
||||
fileBuffer = resized ? (await resized.toBuffer()) : file.data;
|
||||
const { mime, ext } = await fromBuffer(fileBuffer);
|
||||
mimeType = mime;
|
||||
fileSize = fileBuffer.length;
|
||||
const baseFilename = sanitize(file.name.substring(0, file.name.lastIndexOf('.')) || file.name);
|
||||
fsSafeName = `${baseFilename}.${ext}`;
|
||||
|
||||
if (!overwriteExistingFiles) {
|
||||
fsSafeName = await getSafeFileName(Model, staticPath, fsSafeName);
|
||||
}
|
||||
|
||||
await saveBufferToFile(fileBuffer, `${staticPath}/${fsSafeName}`);
|
||||
}
|
||||
|
||||
fileData.filename = fsSafeName;
|
||||
fileData.filesize = file.size;
|
||||
fileData.mimeType = file.mimetype || mime.getType(fsSafeName);
|
||||
fileData.filename = fsSafeName || (!overwriteExistingFiles ? await getSafeFileName(Model, staticPath, file.name) : file.name);
|
||||
fileData.filesize = fileSize || file.size;
|
||||
fileData.mimeType = mimeType || (await fromBuffer(file.data)).mime;
|
||||
|
||||
if (isImage(file.mimetype)) {
|
||||
const dimensions = await getImageSize(file);
|
||||
|
||||
Reference in New Issue
Block a user