diff --git a/.changeset/silent-emus-rush.md b/.changeset/silent-emus-rush.md new file mode 100644 index 0000000000..ac551d20fb --- /dev/null +++ b/.changeset/silent-emus-rush.md @@ -0,0 +1,5 @@ +--- +'@directus/api': patch +--- + +Ensured `ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION` is also respected for extraction of metadata during image upload diff --git a/api/src/services/assets.ts b/api/src/services/assets.ts index 201b0ff628..4d2a9747fa 100644 --- a/api/src/services/assets.ts +++ b/api/src/services/assets.ts @@ -13,7 +13,6 @@ import { contentType } from 'mime-types'; import type { Readable } from 'node:stream'; import hash from 'object-hash'; import path from 'path'; -import type { FailOnOptions } from 'sharp'; import sharp from 'sharp'; import { SUPPORTED_IMAGE_TRANSFORM_FORMATS } from '../constants.js'; import getDatabase from '../database/index.js'; @@ -25,6 +24,7 @@ import { isValidUuid } from '../utils/is-valid-uuid.js'; import * as TransformationUtils from '../utils/transformations.js'; import { AuthorizationService } from './authorization.js'; import { FilesService } from './files.js'; +import { getSharpInstance } from './files/lib/get-sharp-instance.js'; const env = useEnv(); const logger = useLogger(); @@ -162,11 +162,7 @@ export class AssetsService { const readStream = await storage.location(file.storage).read(file.filename_disk, range); - const transformer = sharp({ - limitInputPixels: Math.pow(env['ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION'] as number, 2), - sequentialRead: true, - failOn: env['ASSETS_INVALID_IMAGE_SENSITIVITY_LEVEL'] as FailOnOptions, - }); + const transformer = getSharpInstance(); transformer.timeout({ seconds: clamp(Math.round(getMilliseconds(env['ASSETS_TRANSFORM_TIMEOUT'], 0) / 1000), 1, 3600), diff --git a/api/src/services/files/lib/get-sharp-instance.test.ts b/api/src/services/files/lib/get-sharp-instance.test.ts new file mode 100644 index 0000000000..b14df6f9c6 --- /dev/null +++ b/api/src/services/files/lib/get-sharp-instance.test.ts @@ -0,0 +1,37 @@ +import { useEnv } from '@directus/env'; +import { getSharpInstance } from './get-sharp-instance'; + +import { beforeAll, expect, test, vi } from 'vitest'; + +vi.mock('@directus/env'); + +vi.mock('sharp', () => { + const sharp = { + // using object with default property to mock default import + default: vi.fn(), + }; + + return sharp; +}); + +const ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION = 94906265; +const ASSETS_INVALID_IMAGE_SENSITIVITY_LEVEL = 'error'; + +beforeAll(() => { + vi.mocked(useEnv).mockReturnValue({ + ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION, + ASSETS_INVALID_IMAGE_SENSITIVITY_LEVEL, + }); +}); + +test('getSharpInstance should apply the correct options', async () => { + const sharp = await import('sharp'); + + getSharpInstance(); + + expect(sharp.default).toHaveBeenCalledWith({ + limitInputPixels: Math.pow(ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION, 2), + sequentialRead: true, + failOn: ASSETS_INVALID_IMAGE_SENSITIVITY_LEVEL, + }); +}); diff --git a/api/src/services/files/lib/get-sharp-instance.ts b/api/src/services/files/lib/get-sharp-instance.ts new file mode 100644 index 0000000000..d0cf7fd46b --- /dev/null +++ b/api/src/services/files/lib/get-sharp-instance.ts @@ -0,0 +1,12 @@ +import { useEnv } from '@directus/env'; +import sharp, { type FailOnOptions, type Sharp } from 'sharp'; + +export function getSharpInstance(): Sharp { + const env = useEnv(); + + return sharp({ + limitInputPixels: Math.trunc(Math.pow(env['ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION'] as number, 2)), + sequentialRead: true, + failOn: env['ASSETS_INVALID_IMAGE_SENSITIVITY_LEVEL'] as FailOnOptions, + }); +} diff --git a/api/src/services/files/utils/get-metadata.ts b/api/src/services/files/utils/get-metadata.ts index 746bc43e9b..49a08c4bc4 100644 --- a/api/src/services/files/utils/get-metadata.ts +++ b/api/src/services/files/utils/get-metadata.ts @@ -4,10 +4,10 @@ import { type IccProfile, parse as parseIcc } from 'icc'; import { pick } from 'lodash-es'; import type { Readable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; -import sharp from 'sharp'; import { useEnv } from '@directus/env'; import { useLogger } from '../../../logger/index.js'; import { parseIptc, parseXmp } from './parse-image-metadata.js'; +import { getSharpInstance } from '../lib/get-sharp-instance.js'; const env = useEnv(); const logger = useLogger(); @@ -18,10 +18,12 @@ export async function getMetadata( stream: Readable, allowList: string | string[] = env['FILE_METADATA_ALLOW_LIST'] as string[], ): Promise { + const transformer = getSharpInstance(); + return new Promise((resolve, reject) => { pipeline( stream, - sharp().metadata(async (err, sharpMetadata) => { + transformer.metadata(async (err, sharpMetadata) => { if (err) { reject(err); return; diff --git a/contributors.yml b/contributors.yml index 06f8a81edd..42c49deb00 100644 --- a/contributors.yml +++ b/contributors.yml @@ -142,4 +142,5 @@ - brandondrew - alantiller - SP12893678 +- AndriyAntonenko - jacobwise