2020-10-16 13:10:01 +02:00
import { mediaType } from '@hapi/accept'
import { createHash } from 'crypto'
2021-02-18 11:23:24 +01:00
import { createReadStream , promises } from 'fs'
import { getOrientation , Orientation } from 'get-orientation'
import { IncomingMessage , ServerResponse } from 'http'
2020-10-17 21:22:10 +02:00
// @ts-ignore no types for is-animated
import isAnimated from 'next/dist/compiled/is-animated'
2021-02-18 11:23:24 +01:00
import { join } from 'path'
2020-10-19 19:03:35 +02:00
import Stream from 'stream'
2021-02-18 11:23:24 +01:00
import nodeUrl , { UrlWithParsedQuery } from 'url'
2021-06-30 13:44:40 +02:00
import { NextConfig } from './config-shared'
import { fileExists } from '../lib/file-exists'
2020-11-12 20:24:08 +01:00
import { ImageConfig , imageConfigDefault } from './image-config'
2021-03-18 16:51:36 +01:00
import { processBuffer , Operation } from './lib/squoosh/main'
2021-02-18 11:23:24 +01:00
import Server from './next-server'
import { sendEtagResponse } from './send-payload'
import { getContentType , getExtension } from './serve-static'
2021-07-23 01:11:17 +02:00
import chalk from 'chalk'
2020-10-16 13:10:01 +02:00
//const AVIF = 'image/avif'
const WEBP = 'image/webp'
const PNG = 'image/png'
const JPEG = 'image/jpeg'
2020-10-17 21:22:10 +02:00
const GIF = 'image/gif'
2020-10-24 03:26:52 +02:00
const SVG = 'image/svg+xml'
2021-06-30 23:26:20 +02:00
const CACHE_VERSION = 3
2020-11-02 00:53:36 +01:00
const MODERN_TYPES = [ /* AVIF, */ WEBP ]
2020-10-17 21:22:10 +02:00
const ANIMATABLE_TYPES = [ WEBP , PNG , GIF ]
2020-10-24 03:26:52 +02:00
const VECTOR_TYPES = [ SVG ]
2021-07-10 22:27:14 +02:00
const BLUR_IMG_SIZE = 8 // should match `next-image-loader`
2021-04-13 00:38:51 +02:00
const inflightRequests = new Map < string , Promise < undefined > > ( )
2021-07-23 01:11:17 +02:00
let sharp :
| ( (
input? : string | Buffer ,
options? : import ( 'sharp' ) . SharpOptions
) = > import ( 'sharp' ) . Sharp )
| undefined
try {
sharp = require ( process . env . NEXT_SHARP_PATH || 'sharp' )
} catch ( e ) {
// Sharp not present on the server, Squoosh fallback will be used
}
let shouldShowSharpWarning = process . env . NODE_ENV === 'production'
2020-10-16 13:10:01 +02:00
export async function imageOptimizer (
server : Server ,
req : IncomingMessage ,
res : ServerResponse ,
2021-03-31 18:37:44 +02:00
parsedUrl : UrlWithParsedQuery ,
nextConfig : NextConfig ,
2021-06-30 23:26:20 +02:00
distDir : string ,
isDev = false
2020-10-16 13:10:01 +02:00
) {
2020-11-12 20:24:08 +01:00
const imageData : ImageConfig = nextConfig . images || imageConfigDefault
2021-07-15 21:55:12 +02:00
const {
deviceSizes = [ ] ,
imageSizes = [ ] ,
domains = [ ] ,
loader ,
minimumCacheTTL = 60 ,
} = imageData
2020-10-25 05:54:22 +01:00
if ( loader !== 'default' ) {
await server . render404 ( req , res , parsedUrl )
return { finished : true }
}
2020-10-16 13:10:01 +02:00
const { headers } = req
2021-06-09 00:05:02 +02:00
const { url , w , q } = parsedUrl . query
2020-11-02 00:53:36 +01:00
const mimeType = getSupportedMimeType ( MODERN_TYPES , headers . accept )
2020-10-19 19:03:35 +02:00
let href : string
2020-10-16 13:10:01 +02:00
if ( ! url ) {
res . statusCode = 400
res . end ( '"url" parameter is required' )
return { finished : true }
} else if ( Array . isArray ( url ) ) {
res . statusCode = 400
res . end ( '"url" parameter cannot be an array' )
return { finished : true }
}
2020-10-19 19:03:35 +02:00
let isAbsolute : boolean
if ( url . startsWith ( '/' ) ) {
href = url
isAbsolute = false
} else {
let hrefParsed : URL
2020-10-16 13:10:01 +02:00
try {
2020-10-19 19:03:35 +02:00
hrefParsed = new URL ( url )
href = hrefParsed . toString ( )
isAbsolute = true
} catch ( _error ) {
2020-10-16 13:10:01 +02:00
res . statusCode = 400
res . end ( '"url" parameter is invalid' )
return { finished : true }
}
2020-10-19 19:03:35 +02:00
if ( ! [ 'http:' , 'https:' ] . includes ( hrefParsed . protocol ) ) {
res . statusCode = 400
res . end ( '"url" parameter is invalid' )
return { finished : true }
}
2020-10-16 13:10:01 +02:00
2020-10-19 19:03:35 +02:00
if ( ! domains . includes ( hrefParsed . hostname ) ) {
res . statusCode = 400
res . end ( '"url" parameter is not allowed' )
return { finished : true }
}
2020-10-16 13:10:01 +02:00
}
if ( ! w ) {
res . statusCode = 400
res . end ( '"w" parameter (width) is required' )
return { finished : true }
} else if ( Array . isArray ( w ) ) {
res . statusCode = 400
res . end ( '"w" parameter (width) cannot be an array' )
return { finished : true }
}
if ( ! q ) {
res . statusCode = 400
res . end ( '"q" parameter (quality) is required' )
return { finished : true }
} else if ( Array . isArray ( q ) ) {
res . statusCode = 400
res . end ( '"q" parameter (quality) cannot be an array' )
return { finished : true }
}
2021-06-09 00:05:02 +02:00
// Should match output from next-image-loader
const isStatic = url . startsWith ( '/_next/static/image' )
2021-06-04 10:06:00 +02:00
2020-10-16 13:10:01 +02:00
const width = parseInt ( w , 10 )
if ( ! width || isNaN ( width ) ) {
res . statusCode = 400
res . end ( '"w" parameter (width) must be a number greater than 0' )
return { finished : true }
}
2021-01-04 19:09:07 +01:00
const sizes = [ . . . deviceSizes , . . . imageSizes ]
2021-07-10 22:27:14 +02:00
if ( isDev ) {
sizes . push ( BLUR_IMG_SIZE )
}
2020-10-16 13:10:01 +02:00
if ( ! sizes . includes ( width ) ) {
res . statusCode = 400
res . end ( ` "w" parameter (width) of ${ width } is not allowed ` )
return { finished : true }
}
const quality = parseInt ( q )
if ( isNaN ( quality ) || quality < 1 || quality > 100 ) {
res . statusCode = 400
res . end ( '"q" parameter (quality) must be a number between 1 and 100' )
return { finished : true }
}
const hash = getHash ( [ CACHE_VERSION , href , width , quality , mimeType ] )
const imagesDir = join ( distDir , 'cache' , 'images' )
const hashDir = join ( imagesDir , hash )
const now = Date . now ( )
2021-04-13 00:38:51 +02:00
// If there're concurrent requests hitting the same resource and it's still
// being optimized, wait before accessing the cache.
if ( inflightRequests . has ( hash ) ) {
await inflightRequests . get ( hash )
}
let dedupeResolver : ( val? : PromiseLike < undefined > ) = > void
inflightRequests . set (
hash ,
new Promise ( ( resolve ) = > ( dedupeResolver = resolve ) )
)
try {
if ( await fileExists ( hashDir , 'directory' ) ) {
const files = await promises . readdir ( hashDir )
for ( let file of files ) {
2021-06-30 23:26:20 +02:00
const [ maxAgeStr , expireAtSt , etag , extension ] = file . split ( '.' )
const maxAge = Number ( maxAgeStr )
const expireAt = Number ( expireAtSt )
2021-04-13 00:38:51 +02:00
const contentType = getContentType ( extension )
const fsPath = join ( hashDir , file )
if ( now < expireAt ) {
2021-06-30 23:26:20 +02:00
const result = setResponseHeaders (
req ,
res ,
2021-07-28 01:22:48 +02:00
url ,
2021-06-30 23:26:20 +02:00
etag ,
maxAge ,
contentType ,
isStatic ,
isDev
2021-06-09 01:17:54 +02:00
)
2021-06-30 23:26:20 +02:00
if ( ! result . finished ) {
createReadStream ( fsPath ) . pipe ( res )
2021-04-13 00:38:51 +02:00
}
2020-11-10 05:40:26 +01:00
return { finished : true }
2021-04-13 00:38:51 +02:00
} else {
await promises . unlink ( fsPath )
2020-11-10 05:40:26 +01:00
}
2020-10-16 13:10:01 +02:00
}
}
2021-04-13 00:38:51 +02:00
let upstreamBuffer : Buffer
let upstreamType : string | null
let maxAge : number
if ( isAbsolute ) {
const upstreamRes = await fetch ( href )
2020-10-19 19:03:35 +02:00
2021-04-13 00:38:51 +02:00
if ( ! upstreamRes . ok ) {
res . statusCode = upstreamRes . status
res . end ( '"url" parameter is valid but upstream response is invalid' )
return { finished : true }
}
2020-10-19 19:03:35 +02:00
res . statusCode = upstreamRes . status
2021-04-13 00:38:51 +02:00
upstreamBuffer = Buffer . from ( await upstreamRes . arrayBuffer ( ) )
2021-06-29 01:52:04 +02:00
upstreamType =
detectContentType ( upstreamBuffer ) ||
upstreamRes . headers . get ( 'Content-Type' )
2021-07-20 00:38:03 +02:00
maxAge = getMaxAge ( upstreamRes . headers . get ( 'Cache-Control' ) )
2021-04-13 00:38:51 +02:00
} else {
try {
const resBuffers : Buffer [ ] = [ ]
const mockRes : any = new Stream . Writable ( )
2020-10-16 13:10:01 +02:00
2021-04-17 23:03:08 +02:00
const isStreamFinished = new Promise ( function ( resolve , reject ) {
mockRes . on ( 'finish' , ( ) = > resolve ( true ) )
mockRes . on ( 'end' , ( ) = > resolve ( true ) )
mockRes . on ( 'error' , ( ) = > reject ( ) )
} )
2021-04-13 00:38:51 +02:00
mockRes . write = ( chunk : Buffer | string ) = > {
resBuffers . push ( Buffer . isBuffer ( chunk ) ? chunk : Buffer.from ( chunk ) )
}
mockRes . _write = ( chunk : Buffer | string ) = > {
mockRes . write ( chunk )
}
2020-10-19 19:03:35 +02:00
2021-04-13 00:38:51 +02:00
const mockHeaders : Record < string , string | string [ ] > = { }
mockRes . writeHead = ( _status : any , _headers : any ) = >
Object . assign ( mockHeaders , _headers )
mockRes . getHeader = ( name : string ) = > mockHeaders [ name . toLowerCase ( ) ]
mockRes . getHeaders = ( ) = > mockHeaders
mockRes . getHeaderNames = ( ) = > Object . keys ( mockHeaders )
mockRes . setHeader = ( name : string , value : string | string [ ] ) = >
( mockHeaders [ name . toLowerCase ( ) ] = value )
mockRes . _implicitHeader = ( ) = > { }
2021-07-16 17:11:12 +02:00
mockRes . connection = res . connection
2021-04-13 00:38:51 +02:00
mockRes . finished = false
mockRes . statusCode = 200
2021-04-17 23:03:08 +02:00
const mockReq : any = new Stream . Readable ( )
mockReq . _read = ( ) = > {
mockReq . emit ( 'end' )
mockReq . emit ( 'close' )
return Buffer . from ( '' )
}
mockReq . headers = req . headers
mockReq . method = req . method
mockReq . url = href
2021-07-16 17:11:12 +02:00
mockReq . connection = req . connection
2021-04-17 23:03:08 +02:00
2021-04-13 00:38:51 +02:00
await server . getRequestHandler ( ) (
2021-04-17 23:03:08 +02:00
mockReq ,
2021-04-13 00:38:51 +02:00
mockRes ,
nodeUrl . parse ( href , true )
)
2021-04-17 23:03:08 +02:00
await isStreamFinished
2021-04-13 00:38:51 +02:00
res . statusCode = mockRes . statusCode
upstreamBuffer = Buffer . concat ( resBuffers )
2021-06-29 01:52:04 +02:00
upstreamType =
detectContentType ( upstreamBuffer ) || mockRes . getHeader ( 'Content-Type' )
2021-07-20 00:38:03 +02:00
maxAge = getMaxAge ( mockRes . getHeader ( 'Cache-Control' ) )
2021-04-13 00:38:51 +02:00
} catch ( err ) {
res . statusCode = 500
res . end ( '"url" parameter is valid but upstream response is invalid' )
return { finished : true }
2020-10-19 19:03:35 +02:00
}
}
2020-10-16 13:10:01 +02:00
2021-07-20 00:38:03 +02:00
const expireAt = Math . max ( maxAge , minimumCacheTTL ) * 1000 + now
2021-01-13 19:06:04 +01:00
2021-04-13 00:38:51 +02:00
if ( upstreamType ) {
const vector = VECTOR_TYPES . includes ( upstreamType )
const animate =
ANIMATABLE_TYPES . includes ( upstreamType ) && isAnimated ( upstreamBuffer )
if ( vector || animate ) {
2021-06-30 23:26:20 +02:00
await writeToCacheDir (
hashDir ,
upstreamType ,
maxAge ,
expireAt ,
upstreamBuffer
)
sendResponse (
req ,
res ,
2021-07-28 01:22:48 +02:00
url ,
2021-06-30 23:26:20 +02:00
maxAge ,
upstreamType ,
upstreamBuffer ,
isStatic ,
isDev
)
2021-04-13 00:38:51 +02:00
return { finished : true }
}
2021-03-24 18:59:00 +01:00
2021-04-13 00:38:51 +02:00
if ( ! upstreamType . startsWith ( 'image/' ) ) {
res . statusCode = 400
res . end ( "The requested resource isn't a valid image." )
return { finished : true }
}
2021-03-24 18:59:00 +01:00
}
2020-10-17 21:22:10 +02:00
2021-04-13 00:38:51 +02:00
let contentType : string
2020-10-24 03:26:52 +02:00
2021-04-13 00:38:51 +02:00
if ( mimeType ) {
contentType = mimeType
} else if (
upstreamType ? . startsWith ( 'image/' ) &&
getExtension ( upstreamType )
) {
contentType = upstreamType
} else {
contentType = JPEG
}
try {
2021-07-23 01:11:17 +02:00
let optimizedBuffer : Buffer | undefined
if ( sharp ) {
// Begin sharp transformation logic
const transformer = sharp ( upstreamBuffer )
2021-03-18 16:51:36 +01:00
2021-07-23 01:11:17 +02:00
transformer . rotate ( )
const { width : metaWidth } = await transformer . metadata ( )
if ( metaWidth && metaWidth > width ) {
transformer . resize ( width )
}
if ( contentType === WEBP ) {
transformer . webp ( { quality } )
} else if ( contentType === PNG ) {
transformer . png ( { quality } )
} else if ( contentType === JPEG ) {
transformer . jpeg ( { quality } )
}
2021-03-18 16:51:36 +01:00
2021-07-23 01:11:17 +02:00
optimizedBuffer = await transformer . toBuffer ( )
// End sharp transformation logic
2021-04-13 00:38:51 +02:00
} else {
2021-07-23 01:11:17 +02:00
// Show sharp warning in production once
if ( shouldShowSharpWarning ) {
console . warn (
chalk . yellow . bold ( 'Warning: ' ) +
2021-07-23 16:07:09 +02:00
` For production Image Optimization with Next.js, the optional 'sharp' package is strongly recommended. Run 'yarn add sharp', and Next.js will use it automatically for Image Optimization. \ n ` +
2021-07-23 01:11:17 +02:00
'Read more: https://nextjs.org/docs/messages/sharp-missing-in-production'
)
shouldShowSharpWarning = false
}
2020-10-22 22:39:24 +02:00
2021-07-23 01:11:17 +02:00
// Begin Squoosh transformation logic
const orientation = await getOrientation ( upstreamBuffer )
2021-04-13 00:38:51 +02:00
2021-07-23 01:11:17 +02:00
const operations : Operation [ ] = [ ]
if ( orientation === Orientation . RIGHT_TOP ) {
operations . push ( { type : 'rotate' , numRotations : 1 } )
} else if ( orientation === Orientation . BOTTOM_RIGHT ) {
operations . push ( { type : 'rotate' , numRotations : 2 } )
} else if ( orientation === Orientation . LEFT_BOTTOM ) {
operations . push ( { type : 'rotate' , numRotations : 3 } )
} else {
// TODO: support more orientations
// eslint-disable-next-line @typescript-eslint/no-unused-vars
// const _: never = orientation
}
operations . push ( { type : 'resize' , width } )
//if (contentType === AVIF) {
//} else
if ( contentType === WEBP ) {
optimizedBuffer = await processBuffer (
upstreamBuffer ,
operations ,
'webp' ,
quality
)
} else if ( contentType === PNG ) {
optimizedBuffer = await processBuffer (
upstreamBuffer ,
operations ,
'png' ,
quality
)
} else if ( contentType === JPEG ) {
optimizedBuffer = await processBuffer (
upstreamBuffer ,
operations ,
'jpeg' ,
quality
)
}
2020-10-22 22:39:24 +02:00
2021-07-23 01:11:17 +02:00
// End Squoosh transformation logic
}
2021-04-13 00:38:51 +02:00
if ( optimizedBuffer ) {
2021-06-30 23:26:20 +02:00
await writeToCacheDir (
hashDir ,
contentType ,
maxAge ,
expireAt ,
optimizedBuffer
)
sendResponse (
req ,
res ,
2021-07-28 01:22:48 +02:00
url ,
2021-06-30 23:26:20 +02:00
maxAge ,
contentType ,
optimizedBuffer ,
isStatic ,
isDev
)
2021-04-13 00:38:51 +02:00
} else {
throw new Error ( 'Unable to optimize buffer' )
}
} catch ( error ) {
2021-06-30 23:26:20 +02:00
sendResponse (
req ,
res ,
2021-07-28 01:22:48 +02:00
url ,
2021-06-30 23:26:20 +02:00
maxAge ,
upstreamType ,
upstreamBuffer ,
isStatic ,
isDev
)
2021-02-18 11:23:24 +01:00
}
2020-10-16 13:10:01 +02:00
2021-04-13 00:38:51 +02:00
return { finished : true }
} finally {
// Make sure to remove the hash in the end.
dedupeResolver ! ( )
inflightRequests . delete ( hash )
}
2020-10-16 13:10:01 +02:00
}
2021-01-13 19:06:04 +01:00
async function writeToCacheDir (
dir : string ,
contentType : string ,
2021-06-30 23:26:20 +02:00
maxAge : number ,
2021-01-13 19:06:04 +01:00
expireAt : number ,
buffer : Buffer
) {
await promises . mkdir ( dir , { recursive : true } )
const extension = getExtension ( contentType )
const etag = getHash ( [ buffer ] )
2021-06-30 23:26:20 +02:00
const filename = join ( dir , ` ${ maxAge } . ${ expireAt } . ${ etag } . ${ extension } ` )
2021-01-13 19:06:04 +01:00
await promises . writeFile ( filename , buffer )
}
2021-07-28 01:22:48 +02:00
function getFileNameWithExtension (
url : string ,
contentType : string | null
) : string | void {
const [ urlWithoutQueryParams ] = url . split ( '?' )
const fileNameWithExtension = urlWithoutQueryParams . split ( '/' ) . pop ( )
if ( ! contentType || ! fileNameWithExtension ) {
return
}
const [ fileName ] = fileNameWithExtension . split ( '.' )
const extension = getExtension ( contentType )
return ` ${ fileName } . ${ extension } `
}
2021-06-30 23:26:20 +02:00
function setResponseHeaders (
2020-11-10 05:40:26 +01:00
req : IncomingMessage ,
res : ServerResponse ,
2021-07-28 01:22:48 +02:00
url : string ,
2021-06-30 23:26:20 +02:00
etag : string ,
maxAge : number ,
2020-11-10 05:40:26 +01:00
contentType : string | null ,
2021-06-30 23:26:20 +02:00
isStatic : boolean ,
isDev : boolean
2020-11-10 05:40:26 +01:00
) {
2021-07-01 21:59:16 +02:00
res . setHeader ( 'Vary' , 'Accept' )
2021-06-04 10:06:00 +02:00
res . setHeader (
'Cache-Control' ,
isStatic
2021-06-09 00:05:02 +02:00
? 'public, max-age=315360000, immutable'
2021-06-30 23:26:20 +02:00
: ` public, max-age= ${ isDev ? 0 : maxAge } , must-revalidate `
2021-06-04 10:06:00 +02:00
)
2020-11-10 05:40:26 +01:00
if ( sendEtagResponse ( req , res , etag ) ) {
2021-06-30 23:26:20 +02:00
// already called res.end() so we're finished
return { finished : true }
2020-11-10 05:40:26 +01:00
}
if ( contentType ) {
res . setHeader ( 'Content-Type' , contentType )
}
2021-07-28 01:22:48 +02:00
const fileName = getFileNameWithExtension ( url , contentType )
if ( fileName ) {
res . setHeader ( 'Content-Disposition' , ` inline; filename=" ${ fileName } " ` )
}
2021-06-30 23:26:20 +02:00
return { finished : false }
}
function sendResponse (
req : IncomingMessage ,
res : ServerResponse ,
2021-07-28 01:22:48 +02:00
url : string ,
2021-06-30 23:26:20 +02:00
maxAge : number ,
contentType : string | null ,
buffer : Buffer ,
isStatic : boolean ,
isDev : boolean
) {
const etag = getHash ( [ buffer ] )
const result = setResponseHeaders (
req ,
res ,
2021-07-28 01:22:48 +02:00
url ,
2021-06-30 23:26:20 +02:00
etag ,
maxAge ,
contentType ,
isStatic ,
isDev
)
if ( ! result . finished ) {
res . end ( buffer )
}
2020-11-10 05:40:26 +01:00
}
2020-11-02 00:53:36 +01:00
function getSupportedMimeType ( options : string [ ] , accept = '' ) : string {
const mimeType = mediaType ( accept , options )
return accept . includes ( mimeType ) ? mimeType : ''
}
2020-11-10 05:40:26 +01:00
function getHash ( items : ( string | number | Buffer ) [ ] ) {
2020-10-16 13:10:01 +02:00
const hash = createHash ( 'sha256' )
for ( let item of items ) {
2020-11-10 05:40:26 +01:00
if ( typeof item === 'number' ) hash . update ( String ( item ) )
else {
hash . update ( item )
}
2020-10-16 13:10:01 +02:00
}
// See https://en.wikipedia.org/wiki/Base64#Filenames
return hash . digest ( 'base64' ) . replace ( /\//g , '-' )
}
function parseCacheControl ( str : string | null ) : Map < string , string > {
const map = new Map < string , string > ( )
if ( ! str ) {
return map
}
for ( let directive of str . split ( ',' ) ) {
let [ key , value ] = directive . trim ( ) . split ( '=' )
key = key . toLowerCase ( )
if ( value ) {
value = value . toLowerCase ( )
}
map . set ( key , value )
}
return map
}
2021-06-29 01:52:04 +02:00
/ * *
* Inspects the first few bytes of a buffer to determine if
* it matches the "magic number" of known file signatures .
* https : //en.wikipedia.org/wiki/List_of_file_signatures
* /
2021-07-01 22:53:26 +02:00
export function detectContentType ( buffer : Buffer ) {
2021-06-29 01:52:04 +02:00
if ( [ 0xff , 0xd8 , 0xff ] . every ( ( b , i ) = > buffer [ i ] === b ) ) {
return JPEG
}
if (
[ 0x89 , 0x50 , 0x4e , 0x47 , 0x0d , 0x0a , 0x1a , 0x0a ] . every (
( b , i ) = > buffer [ i ] === b
)
) {
return PNG
}
if ( [ 0x47 , 0x49 , 0x46 , 0x38 ] . every ( ( b , i ) = > buffer [ i ] === b ) ) {
return GIF
}
if (
[ 0x52 , 0x49 , 0x46 , 0x46 , 0 , 0 , 0 , 0 , 0x57 , 0x45 , 0x42 , 0x50 ] . every (
( b , i ) = > ! b || buffer [ i ] === b
)
) {
return WEBP
}
if ( [ 0x3c , 0x3f , 0x78 , 0x6d , 0x6c ] . every ( ( b , i ) = > buffer [ i ] === b ) ) {
return SVG
}
return null
}
2021-07-20 00:38:03 +02:00
export function getMaxAge ( str : string | null ) : number {
2020-10-16 13:10:01 +02:00
const map = parseCacheControl ( str )
if ( map ) {
let age = map . get ( 's-maxage' ) || map . get ( 'max-age' ) || ''
if ( age . startsWith ( '"' ) && age . endsWith ( '"' ) ) {
age = age . slice ( 1 , - 1 )
}
const n = parseInt ( age , 10 )
if ( ! isNaN ( n ) ) {
2021-07-20 00:38:03 +02:00
return n
2020-10-16 13:10:01 +02:00
}
}
2021-07-20 00:38:03 +02:00
return 0
2020-10-16 13:10:01 +02:00
}
2021-07-23 01:11:17 +02:00
export async function resizeImage (
content : Buffer ,
dimension : 'width' | 'height' ,
size : number ,
extension : 'webp' | 'png' | 'jpeg' ,
quality : number
) : Promise < Buffer > {
if ( sharp ) {
const transformer = sharp ( content )
if ( extension === 'webp' ) {
transformer . webp ( { quality } )
} else if ( extension === 'png' ) {
transformer . png ( { quality } )
} else if ( extension === 'jpeg' ) {
transformer . jpeg ( { quality } )
}
if ( dimension === 'width' ) {
transformer . resize ( size )
} else {
transformer . resize ( null , size )
}
const buf = await transformer . toBuffer ( )
return buf
} else {
const resizeOperationOpts : Operation =
dimension === 'width'
? { type : 'resize' , width : size }
: { type : 'resize' , height : size }
const buf = await processBuffer (
content ,
[ resizeOperationOpts ] ,
extension ,
quality
)
return buf
}
}