Skip to content

Commit

Permalink
feat: allow increasing resolution on a per tenant basis (#574)
Browse files Browse the repository at this point in the history
  • Loading branch information
fenos authored Oct 21, 2024
1 parent f950fc4 commit f0c6953
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 5 deletions.
2 changes: 2 additions & 0 deletions migrations/multitenant/0012-image-transformation-limits.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

ALTER TABLE tenants ADD COLUMN image_transformation_max_resolution int NULL;
17 changes: 16 additions & 1 deletion src/http/routes/admin/tenants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ const patchSchema = {
type: 'object',
properties: {
enabled: { type: 'boolean' },
maxResolution: { type: 'number', nullable: true },
},
},
},
},
},
},
optional: ['tracingMode'],
optional: ['tracingMode', 'maxResolution'],
} as const

const schema = {
Expand Down Expand Up @@ -75,6 +76,7 @@ interface tenantDBInterface {
service_key: string
file_size_limit?: number
feature_image_transformation?: boolean
image_transformation_max_resolution?: number
}

export default async function routes(fastify: FastifyInstance) {
Expand All @@ -94,6 +96,7 @@ export default async function routes(fastify: FastifyInstance) {
jwks,
service_key,
feature_image_transformation,
image_transformation_max_resolution,
migrations_version,
migrations_status,
tracing_mode,
Expand All @@ -113,6 +116,7 @@ export default async function routes(fastify: FastifyInstance) {
features: {
imageTransformation: {
enabled: feature_image_transformation,
maxResolution: image_transformation_max_resolution,
},
},
})
Expand All @@ -134,6 +138,7 @@ export default async function routes(fastify: FastifyInstance) {
jwks,
service_key,
feature_image_transformation,
image_transformation_max_resolution,
migrations_version,
migrations_status,
tracing_mode,
Expand All @@ -156,6 +161,7 @@ export default async function routes(fastify: FastifyInstance) {
features: {
imageTransformation: {
enabled: feature_image_transformation,
maxResolution: image_transformation_max_resolution,
},
},
migrationVersion: migrations_version,
Expand Down Expand Up @@ -244,6 +250,10 @@ export default async function routes(fastify: FastifyInstance) {
jwks,
service_key: serviceKey !== undefined ? encrypt(serviceKey) : undefined,
feature_image_transformation: features?.imageTransformation?.enabled,
image_transformation_max_resolution:
features?.imageTransformation?.maxResolution === null
? null
: features?.imageTransformation?.maxResolution,
tracing_mode: tracingMode,
})
.where('id', tenantId)
Expand Down Expand Up @@ -300,6 +310,11 @@ export default async function routes(fastify: FastifyInstance) {
tenantInfo.feature_image_transformation = features?.imageTransformation?.enabled
}

if (typeof features?.imageTransformation?.maxResolution !== 'undefined') {
tenantInfo.image_transformation_max_resolution = features?.imageTransformation
?.image_transformation_max_resolution as number | undefined
}

if (databasePoolUrl) {
tenantInfo.database_pool_url = encrypt(databasePoolUrl)
}
Expand Down
10 changes: 9 additions & 1 deletion src/http/routes/render/renderAuthenticatedImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { FastifyInstance } from 'fastify'
import { ImageRenderer } from '@storage/renderer'
import { transformationOptionsSchema } from '../../schemas/transformations'
import { ROUTE_OPERATIONS } from '../operations'
import { getTenantConfig } from '@internal/database'

const { storageS3Bucket } = getConfig()
const { storageS3Bucket, isMultitenant } = getConfig()

const renderAuthenticatedImageParamsSchema = {
type: 'object',
Expand Down Expand Up @@ -56,6 +57,13 @@ export default async function routes(fastify: FastifyInstance) {

const renderer = request.storage.renderer('image') as ImageRenderer

if (isMultitenant) {
const tenantConfig = await getTenantConfig(request.tenantId)
renderer.setLimits({
maxResolution: tenantConfig.features.imageTransformation.maxResolution,
})
}

return renderer.setTransformations(request.query).render(request, response, {
bucket: storageS3Bucket,
key: s3Key,
Expand Down
10 changes: 9 additions & 1 deletion src/http/routes/render/renderPublicImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { FastifyInstance } from 'fastify'
import { ImageRenderer } from '@storage/renderer'
import { transformationOptionsSchema } from '../../schemas/transformations'
import { ROUTE_OPERATIONS } from '../operations'
import { getTenantConfig } from '@internal/database'

const { storageS3Bucket } = getConfig()
const { storageS3Bucket, isMultitenant } = getConfig()

const renderPublicImageParamsSchema = {
type: 'object',
Expand Down Expand Up @@ -61,6 +62,13 @@ export default async function routes(fastify: FastifyInstance) {

const renderer = request.storage.renderer('image') as ImageRenderer

if (isMultitenant) {
const tenantConfig = await getTenantConfig(request.tenantId)
renderer.setLimits({
maxResolution: tenantConfig.features.imageTransformation.maxResolution,
})
}

return renderer.setTransformations(request.query).render(request, response, {
bucket: storageS3Bucket,
key: s3Key,
Expand Down
12 changes: 10 additions & 2 deletions src/http/routes/render/renderSignedImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { FromSchema } from 'json-schema-to-ts'
import { FastifyInstance } from 'fastify'

import { SignedToken, verifyJWT } from '@internal/auth'
import { getJwtSecret } from '@internal/database'
import { getJwtSecret, getTenantConfig } from '@internal/database'
import { ERRORS } from '@internal/errors'

import { ImageRenderer } from '@storage/renderer'
import { getConfig } from '../../../config'
import { ROUTE_OPERATIONS } from '../operations'

const { storageS3Bucket } = getConfig()
const { storageS3Bucket, isMultitenant } = getConfig()

const renderAuthenticatedImageParamsSchema = {
type: 'object',
Expand Down Expand Up @@ -86,6 +86,14 @@ export default async function routes(fastify: FastifyInstance) {
.findObject(objParts.join('/'), 'id,version')

const renderer = request.storage.renderer('image') as ImageRenderer

if (isMultitenant) {
const tenantConfig = await getTenantConfig(request.tenantId)
renderer.setLimits({
maxResolution: tenantConfig.features.imageTransformation.maxResolution,
})
}

return renderer
.setTransformationsFromString(transformations || '')
.render(request, response, {
Expand Down
3 changes: 3 additions & 0 deletions src/internal/database/tenant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ interface TenantConfig {
export interface Features {
imageTransformation: {
enabled: boolean
maxResolution?: number
}
}

Expand Down Expand Up @@ -202,6 +203,7 @@ export async function getTenantConfig(tenantId: string): Promise<TenantConfig> {
jwks,
service_key,
feature_image_transformation,
image_transformation_max_resolution,
database_pool_url,
max_connections,
migrations_version,
Expand All @@ -227,6 +229,7 @@ export async function getTenantConfig(tenantId: string): Promise<TenantConfig> {
features: {
imageTransformation: {
enabled: feature_image_transformation,
maxResolution: image_transformation_max_resolution,
},
},
migrationVersion: migrations_version,
Expand Down
21 changes: 21 additions & 0 deletions src/storage/renderer/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ axiosRetry(client, {
},
})

interface TransformLimits {
maxResolution?: number
}

/**
* ImageRenderer
* renders an image by applying transformations
Expand All @@ -77,6 +81,7 @@ axiosRetry(client, {
export class ImageRenderer extends Renderer {
private readonly client: Axios
private transformOptions?: TransformOptions
private limits?: TransformLimits

constructor(private readonly backend: StorageBackendAdapter) {
super()
Expand Down Expand Up @@ -118,6 +123,15 @@ export class ImageRenderer extends Renderer {
return segments
}

static applyTransformationLimits(limits: TransformLimits) {
const transforms: string[] = []
if (typeof limits?.maxResolution === 'number') {
transforms.push(`max_src_resolution:${limits.maxResolution}`)
}

return transforms
}

protected static formatResizeType(resize: TransformOptions['resize']) {
const defaultResize = 'fill'

Expand Down Expand Up @@ -149,6 +163,11 @@ export class ImageRenderer extends Renderer {
return this
}

setLimits(limits: TransformLimits) {
this.limits = limits
return this
}

setTransformationsFromString(transformations: string) {
const params = transformations.split(',')

Expand Down Expand Up @@ -190,10 +209,12 @@ export class ImageRenderer extends Renderer {
this.backend.headObject(options.bucket, options.key, options.version),
])
const transformations = ImageRenderer.applyTransformation(this.transformOptions || {})
const transformLimits = ImageRenderer.applyTransformationLimits(this.limits || {})

const url = [
'/public',
...transformations,
...transformLimits,
'plain',
privateURL.startsWith('local://') ? privateURL : encodeURIComponent(privateURL),
]
Expand Down
2 changes: 2 additions & 0 deletions src/test/tenant.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const payload = {
features: {
imageTransformation: {
enabled: true,
maxResolution: null,
},
},
}
Expand All @@ -40,6 +41,7 @@ const payload2 = {
features: {
imageTransformation: {
enabled: false,
maxResolution: null,
},
},
}
Expand Down

0 comments on commit f0c6953

Please sign in to comment.