-
-
Notifications
You must be signed in to change notification settings - Fork 107
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: image resizing with imgproxy backend
- Loading branch information
Showing
17 changed files
with
2,309 additions
and
1,585 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { GenericStorageBackend } from '../backend/generic' | ||
import axios, { Axios } from 'axios' | ||
|
||
interface TransformOptions { | ||
width?: number | ||
height?: number | ||
resize?: 'fill' | 'fit' | 'fill-down' | 'force' | 'auto' | ||
} | ||
|
||
interface ImgProxyOptions { | ||
url: string | ||
} | ||
|
||
const LIMITS = { | ||
height: { | ||
max: 5000, | ||
min: 20, | ||
}, | ||
width: { | ||
max: 5000, | ||
min: 20, | ||
}, | ||
} | ||
|
||
export class Imgproxy { | ||
private client: Axios | ||
|
||
constructor( | ||
private readonly backend: GenericStorageBackend, | ||
private readonly options: ImgProxyOptions | ||
) { | ||
this.client = axios.create({ | ||
baseURL: options.url, | ||
timeout: 8000, | ||
}) | ||
} | ||
|
||
getClient() { | ||
return this.client | ||
} | ||
|
||
async transform(bucket: string, key: string, options: TransformOptions) { | ||
const privateURL = await this.backend.privateAssetUrl(bucket, key) | ||
const urlTransformation = this.applyURLTransformation(options) | ||
|
||
const url = ['/public', ...urlTransformation, 'plain', privateURL] | ||
|
||
return this.getClient().get(url.join('/'), { | ||
responseType: 'arraybuffer', | ||
}) | ||
} | ||
|
||
protected applyURLTransformation(options: TransformOptions) { | ||
const segments = [] | ||
|
||
if (options.height) { | ||
segments.push(`height:${clamp(options.height, LIMITS.height.min, LIMITS.height.max)}`) | ||
} | ||
|
||
if (options.width) { | ||
segments.push(`width:${clamp(options.width, LIMITS.width.min, LIMITS.width.max)}`) | ||
} | ||
|
||
if (options.width || options.height) { | ||
segments.push(`resizing_type:${options.resize || 'fill'}`) | ||
} | ||
|
||
return segments | ||
} | ||
} | ||
|
||
const clamp = (num: number, min: number, max: number) => Math.min(Math.max(num, min), max) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { FastifyInstance } from 'fastify' | ||
import { postgrest, superUserPostgrest } from '../../plugins/postgrest' | ||
import renderPublicImage from './renderPublicImage' | ||
import renderAuthenticatedImage from './renderAuthenticatedImage' | ||
import jwt from '../../plugins/jwt' | ||
|
||
export default async function routes(fastify: FastifyInstance) { | ||
fastify.register(async function authorizationContext(fastify) { | ||
fastify.register(jwt) | ||
fastify.register(postgrest) | ||
|
||
fastify.register(renderAuthenticatedImage) | ||
}) | ||
|
||
fastify.register(async (fastify) => { | ||
fastify.register(superUserPostgrest) | ||
|
||
fastify.register(renderPublicImage) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import { getConfig } from '../../utils/config' | ||
import { GenericStorageBackend } from '../../backend/generic' | ||
import { FileBackend } from '../../backend/file' | ||
import { S3Backend } from '../../backend/s3' | ||
import { FromSchema } from 'json-schema-to-ts' | ||
import { FastifyInstance } from 'fastify' | ||
import { Obj } from '../../types/types' | ||
import { normalizeContentType, transformPostgrestError } from '../../utils' | ||
import { Imgproxy } from '../../renderer/imgproxy' | ||
import { AxiosError } from 'axios' | ||
|
||
const { region, globalS3Bucket, globalS3Endpoint, storageBackendType, imgProxyURL } = getConfig() | ||
|
||
let storageBackend: GenericStorageBackend | ||
|
||
if (storageBackendType === 'file') { | ||
storageBackend = new FileBackend() | ||
} else { | ||
storageBackend = new S3Backend(region, globalS3Endpoint) | ||
} | ||
|
||
const imageRenderer = new Imgproxy(storageBackend, { | ||
url: imgProxyURL || '', | ||
}) | ||
|
||
const renderAuthenticatedImageParamsSchema = { | ||
type: 'object', | ||
properties: { | ||
bucketName: { type: 'string', examples: ['avatars'] }, | ||
'*': { type: 'string', examples: ['folder/cat.png'] }, | ||
}, | ||
required: ['bucketName', '*'], | ||
} as const | ||
|
||
const renderImageQuerySchema = { | ||
type: 'object', | ||
properties: { | ||
height: { type: 'number', examples: [100] }, | ||
width: { type: 'number', examples: [100] }, | ||
resize: { type: 'string', enum: ['fill', 'fit', 'fill-down', 'force', 'auto'] }, | ||
}, | ||
} as const | ||
|
||
interface renderImageRequestInterface { | ||
Params: FromSchema<typeof renderAuthenticatedImageParamsSchema> | ||
Querystring: FromSchema<typeof renderImageQuerySchema> | ||
} | ||
|
||
export default async function routes(fastify: FastifyInstance) { | ||
const summary = 'Render an authenticated image with the given transformations' | ||
fastify.get<renderImageRequestInterface>( | ||
'/authenticated/:bucketName/*', | ||
{ | ||
schema: { | ||
params: renderAuthenticatedImageParamsSchema, | ||
summary, | ||
response: { '4xx': { $ref: 'errorSchema#', description: 'Error response' } }, | ||
tags: ['object'], | ||
}, | ||
}, | ||
async (request, response) => { | ||
const { bucketName } = request.params | ||
const objectName = request.params['*'] | ||
|
||
const objectResponse = await request.postgrest | ||
.from<Obj>('objects') | ||
.select('id') | ||
.match({ | ||
name: objectName, | ||
bucket_id: bucketName, | ||
}) | ||
.single() | ||
|
||
if (objectResponse.error) { | ||
const { status, error } = objectResponse | ||
request.log.error({ error }, 'error object') | ||
return response.status(400).send(transformPostgrestError(error, status)) | ||
} | ||
|
||
const s3Key = `${request.tenantId}/${bucketName}/${objectName}` | ||
|
||
try { | ||
const imageResponse = await imageRenderer.transform(globalS3Bucket, s3Key, request.query) | ||
|
||
response | ||
.status(imageResponse.status) | ||
.header('Accept-Ranges', 'bytes') | ||
.header('Content-Type', normalizeContentType(imageResponse.headers['content-type'])) | ||
.header('Cache-Control', imageResponse.headers['cache-control']) | ||
.header('Content-Length', imageResponse.headers['content-length']) | ||
.header('ETag', imageResponse.headers['etag']) | ||
|
||
return response.send(imageResponse.data) | ||
} catch (err: any) { | ||
if (err instanceof AxiosError) { | ||
return response.status(err.response?.status || 500).send({ | ||
message: err.message, | ||
statusCode: err.response?.status || '500', | ||
error: err.message, | ||
}) | ||
} | ||
|
||
return response.status(500).send({ | ||
message: err.message, | ||
statusCode: '500', | ||
error: err.message, | ||
}) | ||
} | ||
} | ||
) | ||
} |
Oops, something went wrong.