mirror of
https://github.com/diced/zipline.git
synced 2025-12-12 07:40:45 -08:00
feat: compression formats
This commit is contained in:
@@ -1,13 +1,62 @@
|
||||
import sharp from 'sharp';
|
||||
|
||||
export function compressFile(filePath: string, quality: number) {
|
||||
const buffer = sharp(filePath).withMetadata().jpeg({ quality: quality }).toBuffer();
|
||||
export const COMPRESS_TYPES = ['jpg', 'jpeg', 'png', 'webp', 'jxl'] as const;
|
||||
export type CompressType = (typeof COMPRESS_TYPES)[number];
|
||||
|
||||
return buffer.then((data) => {
|
||||
return sharp(data).toFile(filePath);
|
||||
});
|
||||
export type CompressResult = {
|
||||
mimetype: string;
|
||||
ext: CompressType;
|
||||
};
|
||||
|
||||
export type CompressOptions = {
|
||||
quality: number;
|
||||
type?: CompressType;
|
||||
};
|
||||
|
||||
export function checkOutput(type: CompressType): boolean {
|
||||
if (type === 'jpg') type = 'jpeg';
|
||||
|
||||
return !!(sharp.format as any)[type]?.output?.file && !!(sharp.format as any)[type]?.output?.buffer;
|
||||
}
|
||||
|
||||
export function replaceFileNameJpg(original: string, when?: boolean) {
|
||||
return when ? original.replace(/\.[a-zA-Z0-9]+$/, '.jpg') : original;
|
||||
export async function compressFile(filePath: string, options: CompressOptions): Promise<CompressResult> {
|
||||
const { quality, type } = options;
|
||||
|
||||
const image = sharp(filePath).withMetadata();
|
||||
|
||||
const result: CompressResult = {
|
||||
mimetype: '',
|
||||
ext: 'jpg',
|
||||
};
|
||||
|
||||
let buffer: Buffer;
|
||||
|
||||
switch (type?.toLowerCase()) {
|
||||
case 'png':
|
||||
buffer = await image.png({ quality }).toBuffer();
|
||||
result.mimetype = 'image/png';
|
||||
result.ext = 'png';
|
||||
break;
|
||||
case 'webp':
|
||||
buffer = await image.webp({ quality }).toBuffer();
|
||||
result.mimetype = 'image/webp';
|
||||
result.ext = 'webp';
|
||||
break;
|
||||
case 'jxl':
|
||||
buffer = await image.jxl({ quality }).toBuffer();
|
||||
result.mimetype = 'image/jxl';
|
||||
result.ext = 'jxl';
|
||||
break;
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
default:
|
||||
buffer = await image.jpeg({ quality }).toBuffer();
|
||||
result.mimetype = 'image/jpeg';
|
||||
result.ext = 'jpg';
|
||||
break;
|
||||
}
|
||||
|
||||
await sharp(buffer).toFile(filePath);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import ms from 'ms';
|
||||
import { Config } from '../config/validate';
|
||||
import { checkOutput, COMPRESS_TYPES, CompressType } from '../compress';
|
||||
|
||||
// from ms@3.0.0-canary.1
|
||||
type Unit =
|
||||
@@ -42,6 +43,8 @@ type StringBoolean = 'true' | 'false';
|
||||
export type UploadHeaders = {
|
||||
'x-zipline-deletes-at'?: string;
|
||||
'x-zipline-format'?: Config['files']['defaultFormat'];
|
||||
|
||||
'x-zipline-image-compression-type'?: CompressType;
|
||||
'x-zipline-image-compression-percent'?: string;
|
||||
'x-zipline-password'?: string;
|
||||
'x-zipline-max-views'?: string;
|
||||
@@ -67,11 +70,16 @@ export type UploadHeaders = {
|
||||
export type UploadOptions = {
|
||||
deletesAt?: Date | 'never';
|
||||
format?: Config['files']['defaultFormat'];
|
||||
imageCompressionPercent?: number;
|
||||
password?: string;
|
||||
maxViews?: number;
|
||||
noJson?: boolean;
|
||||
addOriginalName?: boolean;
|
||||
|
||||
imageCompression?: {
|
||||
type?: CompressType;
|
||||
percent: number;
|
||||
};
|
||||
|
||||
overrides?: {
|
||||
filename?: string;
|
||||
returnDomain?: string;
|
||||
@@ -128,10 +136,19 @@ export function parseExpiry(header: string): Date | null {
|
||||
return human;
|
||||
}
|
||||
|
||||
function parsePercent(header: keyof UploadHeaders, percent: string) {
|
||||
const num = Number(percent);
|
||||
if (isNaN(num)) return headerError(header, 'Invalid percent (NaN)');
|
||||
|
||||
if (num < 0 || num > 100) return headerError(header, 'Invalid percent (must be between 0 and 100)');
|
||||
|
||||
return num;
|
||||
}
|
||||
|
||||
function headerError(header: keyof UploadHeaders, message: string) {
|
||||
return {
|
||||
header,
|
||||
message,
|
||||
message: `[${header}]: ${message}`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -166,18 +183,41 @@ export function parseHeaders(headers: UploadHeaders, fileConfig: Config['files']
|
||||
}
|
||||
|
||||
const imageCompressionPercent = headers['x-zipline-image-compression-percent'];
|
||||
if (imageCompressionPercent) {
|
||||
const num = Number(imageCompressionPercent);
|
||||
if (isNaN(num))
|
||||
return headerError('x-zipline-image-compression-percent', 'Invalid compression percent (NaN)');
|
||||
|
||||
if (num < 0 || num > 100)
|
||||
const imageCompressionType = headers['x-zipline-image-compression-type'];
|
||||
if (imageCompressionType) {
|
||||
if (!imageCompressionPercent)
|
||||
return headerError(
|
||||
'x-zipline-image-compression-percent',
|
||||
'Invalid compression percent (must be between 0 and 100)',
|
||||
'missing "x-zipline-image-compression-percent" when "x-zipline-image-compression-type" is provided',
|
||||
);
|
||||
|
||||
response.imageCompressionPercent = num;
|
||||
if (!COMPRESS_TYPES.includes(imageCompressionType))
|
||||
return headerError(
|
||||
'x-zipline-image-compression-type',
|
||||
`Invalid compression type (must be one of: ${COMPRESS_TYPES.join(', ')})`,
|
||||
);
|
||||
|
||||
if (!checkOutput(imageCompressionType))
|
||||
return headerError(
|
||||
'x-zipline-image-compression-type',
|
||||
`Compression type "${imageCompressionType}" is not supported on the system.`,
|
||||
);
|
||||
|
||||
const percent = parsePercent('x-zipline-image-compression-percent', imageCompressionPercent);
|
||||
if (typeof percent === 'object') return percent;
|
||||
|
||||
response.imageCompression = {
|
||||
type: imageCompressionType,
|
||||
percent,
|
||||
};
|
||||
} else if (imageCompressionPercent) {
|
||||
const percent = parsePercent('x-zipline-image-compression-percent', imageCompressionPercent);
|
||||
if (typeof percent === 'object') return percent;
|
||||
|
||||
response.imageCompression = {
|
||||
type: 'jpg',
|
||||
percent,
|
||||
};
|
||||
}
|
||||
|
||||
const password = headers['x-zipline-password'];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Prisma } from '@/prisma/client';
|
||||
import { bytes } from '@/lib/bytes';
|
||||
import { compressFile } from '@/lib/compress';
|
||||
import { compressFile, CompressResult } from '@/lib/compress';
|
||||
import { config } from '@/lib/config';
|
||||
import { hashPassword } from '@/lib/crypto';
|
||||
import { datasource } from '@/lib/datasource';
|
||||
@@ -45,7 +45,7 @@ export type ApiUploadResponse = {
|
||||
url: string;
|
||||
pending?: boolean;
|
||||
removedGps?: boolean;
|
||||
compressed?: boolean;
|
||||
compressed?: CompressResult;
|
||||
}[];
|
||||
|
||||
deletesAt?: string;
|
||||
@@ -65,7 +65,7 @@ export default fastifyPlugin(
|
||||
Headers: UploadHeaders;
|
||||
}>(PATH, { preHandler: [userMiddleware, rateLimit] }, async (req, res) => {
|
||||
const options = parseHeaders(req.headers, config.files);
|
||||
if (options.header) return res.badRequest('bad options, receieved: ' + JSON.stringify(options));
|
||||
if (options.header) return res.badRequest(`bad options: ${options.message}`);
|
||||
|
||||
if (options.partial) return res.badRequest('bad options, receieved: partial upload');
|
||||
|
||||
@@ -168,11 +168,13 @@ export default fastifyPlugin(
|
||||
}
|
||||
|
||||
// compress the image if requested
|
||||
let compressed = false;
|
||||
if (mimetype.startsWith('image/') && options.imageCompressionPercent) {
|
||||
await compressFile(file.filepath, options.imageCompressionPercent);
|
||||
logger.c('jpg').debug(`compressed file ${file.filename}`);
|
||||
compressed = true;
|
||||
let compressed;
|
||||
if (mimetype.startsWith('image/') && options.imageCompression) {
|
||||
compressed = await compressFile(file.filepath, {
|
||||
quality: options.imageCompression.percent,
|
||||
type: options.imageCompression.type,
|
||||
});
|
||||
logger.c('compress').debug(`compressed file ${file.filename}`);
|
||||
}
|
||||
|
||||
// remove gps metadata if requested
|
||||
@@ -187,9 +189,9 @@ export default fastifyPlugin(
|
||||
const tempFileStats = await stat(file.filepath);
|
||||
|
||||
const data: Prisma.FileCreateInput = {
|
||||
name: `${fileName}${compressed ? '.jpg' : extension}`,
|
||||
name: `${fileName}${compressed ? '.' + compressed.ext : extension}`,
|
||||
size: tempFileStats.size,
|
||||
type: compressed ? 'image/jpeg' : mimetype,
|
||||
type: compressed?.mimetype ?? mimetype,
|
||||
User: { connect: { id: req.user ? req.user.id : options.folder ? folder?.userId : undefined } },
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user