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:
Dan Ribbens
2022-09-12 13:29:07 -04:00
committed by GitHub
parent 391c9d8682
commit 7acf944a28
15 changed files with 224 additions and 81 deletions

View File

@@ -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(),
),

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
};

View File

@@ -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}`;
};

View File

@@ -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);
}

View File

@@ -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 = {

View 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);