From d6eaaa9f019b96ac7b4a79efa9ef74e40824e2e4 Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Tue, 16 Jul 2024 16:39:30 +0700 Subject: [PATCH] UBERF-7597: Get rid of formats in preview.ts (#6077) Signed-off-by: Andrey Sobolev --- packages/presentation/src/preview.ts | 47 ++++++++++---------------- server/front/readme.md | 7 ++-- server/front/src/index.ts | 49 ++++++++++++++++++++-------- server/front/src/starter.ts | 2 +- 4 files changed, 56 insertions(+), 49 deletions(-) diff --git a/packages/presentation/src/preview.ts b/packages/presentation/src/preview.ts index f3eeabed44..f97b146c7d 100644 --- a/packages/presentation/src/preview.ts +++ b/packages/presentation/src/preview.ts @@ -4,9 +4,6 @@ import { getMetadata } from '@hcengineering/platform' import { getBlobHref, getClient, getCurrentWorkspaceUrl, getFileUrl } from '.' import presentation from './plugin' -type SupportedFormat = string -const defaultSupportedFormats = 'avif,webp,heif,jpeg' - export interface ProviderPreviewConfig { // Identifier of provider // If set to '' could be applied to any provider, for example to exclude some 'image/gif' etc from being processing with providers. @@ -14,8 +11,6 @@ export interface ProviderPreviewConfig { // Preview url // If '' preview is disabled for config. previewUrl: string - // A supported file formats - formats: SupportedFormat[] // Content type markers, will check by containts, if passed, only allow to be used with matched content types. contentTypes?: string[] @@ -28,8 +23,7 @@ export interface PreviewConfig { const defaultPreview = (): ProviderPreviewConfig => ({ providerId: '', - formats: ['avif', 'webp', 'jpg'], - previewUrl: `/files/${getCurrentWorkspaceUrl()}?file=:blobId.:format&size=:size` + previewUrl: `/files/${getCurrentWorkspaceUrl()}?file=:blobId&size=:size` }) /** @@ -39,7 +33,7 @@ const defaultPreview = (): ProviderPreviewConfig => ({ - providerName - a provider name should be same as in Storage configuration. It coult be empty and it will match by content types. -- previewUrl - an Url with :workspace, :blobId, :downloadFile, :size, :format placeholders, they will be replaced in UI with an appropriate blob values. +- previewUrl - an Url with :workspace, :blobId, :downloadFile, :size placeholders, they will be replaced in UI with an appropriate blob values. - supportedFormats - a `,` separated list of file extensions. - contentTypes - a ',' separated list of content type patterns. @@ -58,14 +52,14 @@ export function parsePreviewConfig (config?: string): PreviewConfig | undefined if (c === '') { continue // Skip empty lines } + const vars = c.split('|') let [provider, url, formats, contentTypes] = c.split('|').map((it) => it.trim()) - if (formats === undefined) { - formats = defaultSupportedFormats + if (vars.length === 3) { + contentTypes = formats // Backward compatibility, since formats are obsolete } const p: ProviderPreviewConfig = { providerId: provider, previewUrl: url, - formats: formats.split(',').map((it) => it.trim()), // Allow preview only for images by default contentTypes: contentTypes !== undefined @@ -94,7 +88,6 @@ export function getPreviewConfig (): PreviewConfig { { providerId: '', contentTypes: ['image/gif', 'image/apng', 'image/svg'], // Disable gif and apng format preview. - formats: [], previewUrl: '' } ] @@ -182,25 +175,19 @@ function blobToSrcSet ( url = url.replaceAll(':blobId', encodeURIComponent(blob._id)) let result = '' - for (const f of cfg.formats ?? []) { - if (result.length > 0) { - result += ', ' - } - - const fu = url.replaceAll(':format', f) - - if (width !== undefined) { - result += - fu.replaceAll(':size', `${width}`) + - ' 1x , ' + - fu.replaceAll(':size', `${width * 2}`) + - ' 2x, ' + - fu.replaceAll(':size', `${width * 3}`) + - ' 3x' - } else { - result += fu.replaceAll(':size', `${-1}`) - } + const fu = url + if (width !== undefined) { + result += + fu.replaceAll(':size', `${width}`) + + ' 1x , ' + + fu.replaceAll(':size', `${width * 2}`) + + ' 2x, ' + + fu.replaceAll(':size', `${width * 3}`) + + ' 3x' + } else { + result += fu.replaceAll(':size', `${-1}`) } + return result } diff --git a/server/front/readme.md b/server/front/readme.md index 44fc68d82d..95027189ec 100644 --- a/server/front/readme.md +++ b/server/front/readme.md @@ -28,11 +28,11 @@ A `;` separated list of triples, providerName|previewUrl|supportedFormats. - providerName - a provider name should be same as in Storage configuration. It coult be empty and it will match by content types. -- previewUrl - an Url with :workspace, :blobId, :downloadFile, :size, :format placeholders, they will be replaced in UI with an appropriate blob values. +- previewUrl - an Url with :workspace, :blobId, :downloadFile, :size placeholders, they will be replaced in UI with an appropriate blob values. - supportedFormats - a `,` separated list of file extensions. - contentTypes - a ',' separated list of content type patterns. -PREVIEW_CONFIG=*|https://front.hc.engineering/files/:workspace/api/preview/?format=:format&width=:size&image=:downloadFile +PREVIEW_CONFIG=*|https://front.hc.engineering/files/:workspace/api/preview/?width=:size&image=:downloadFile ## Variables @@ -40,7 +40,6 @@ PREVIEW_CONFIG=*|https://front.hc.engineering/files/:workspace/api/preview/?form - :blobId - an uniq blob _id identifier. - :size - a numeric value to determine required size of the image, image will not be upscaled, only downscaled. If -1 is passed, original image size value will be used. - :downloadFile - an URI encoded component value of full download URI, could be presigned uri to S3 storage. -- :format - an a conversion file format, `avif`,`webp` etc. ## Passing default variant. @@ -50,7 +49,7 @@ providerName could be set to `*` in this case it will be default preview provide If no preview config are specified, a default one targating a front service preview/resize functionality will be used. -`/files/${getCurrentWorkspaceUrl()}?file=:blobId.:format&size=:size` +`/files/${getCurrentWorkspaceUrl()}?file=:blobId&size=:size` ## Testing with dev-production/etc. diff --git a/server/front/src/index.ts b/server/front/src/index.ts index 40c83224ea..44892d6f38 100644 --- a/server/front/src/index.ts +++ b/server/front/src/index.ts @@ -32,13 +32,11 @@ import { v4 as uuid } from 'uuid' import { preConditions } from './utils' import fs from 'fs' +import { Readable } from 'stream' const cacheControlValue = 'public, max-age=365d' const cacheControlNoCache = 'public, no-store, no-cache, must-revalidate, max-age=0' -type SupportedFormat = 'jpeg' | 'avif' | 'heif' | 'webp' | 'png' -const supportedFormats: SupportedFormat[] = ['avif', 'webp', 'heif', 'jpeg', 'png'] - async function storageUpload ( ctx: MeasureContext, storageAdapter: StorageAdapter, @@ -118,6 +116,8 @@ async function getFileRange ( 'Last-Modified': new Date(stat.modifiedOn).toISOString() }) + res.send(Readable.toWeb(dataStream)) + dataStream.pipe(res) await new Promise((resolve, reject) => { @@ -179,7 +179,12 @@ async function getFile ( } if (preConditions.IfUnmodifiedSince(req.headers, { lastModified: new Date(stat.modifiedOn) }) === 'failed') { // Send 412 (Precondition Failed) - res.statusCode = 412 + res.writeHead(412, { + 'content-type': stat.contentType, + etag: stat.etag, + 'last-modified': new Date(stat.modifiedOn).toISOString(), + 'cache-control': cacheControlValue + }) res.end() return } @@ -360,17 +365,12 @@ export function start ( (req.query.token as string | undefined) payload = token !== undefined ? decodeToken(token) : payload - let uuid = req.params.file ?? req.query.file + const uuid = req.params.file ?? req.query.file if (uuid === undefined) { res.status(404).send() return } - const format: SupportedFormat | undefined = supportedFormats.find((it) => uuid.endsWith(it)) - if (format !== undefined) { - uuid = uuid.slice(0, uuid.length - format.length - 1) - } - let blobInfo = await ctx.with( 'notoken-stat', { workspace: payload.workspace.name }, @@ -411,12 +411,13 @@ export function start ( } const size = req.query.size !== undefined ? parseInt(req.query.size as string) : undefined - if (format !== undefined && isImage && blobInfo.contentType !== 'image/gif') { + const accept = req.headers.accept + if (accept !== undefined && isImage && blobInfo.contentType !== 'image/gif') { blobInfo = await ctx.with( 'resize', {}, async (ctx) => - await getGeneratePreview(ctx, blobInfo as PlatformBlob, size ?? -1, uuid, config, payload, format) + await getGeneratePreview(ctx, blobInfo as PlatformBlob, size ?? -1, uuid, config, payload, accept) ) } @@ -731,6 +732,8 @@ export function start ( } } +const supportedFormats = ['avif', 'webp', 'heif', 'jpeg', 'png'] + async function getGeneratePreview ( ctx: MeasureContext, blob: PlatformBlob, @@ -738,11 +741,29 @@ async function getGeneratePreview ( uuid: string, config: { storageAdapter: StorageAdapter }, payload: Token, - format: SupportedFormat = 'jpeg' + accept: string ): Promise { if (size === undefined) { return blob } + + const formats = accept.split(',').map((it) => it.trim()) + + // Select appropriate format + let format: string | undefined + + for (const f of formats) { + const [type] = f.split(';') + const [clazz, kind] = type.split('/') + if (clazz === 'image' && supportedFormats.includes(kind)) { + format = kind + break + } + } + if (format === undefined) { + return blob + } + const sizeId = uuid + `%preview%${size}${format !== 'jpeg' ? format : ''}` const d = await config.storageAdapter.stat(ctx, payload.workspace, sizeId) @@ -823,7 +844,7 @@ async function getGeneratePreview ( Analytics.handleError(err) ctx.error('failed to resize image', { err, - format, + format: accept, contentType: blob.contentType, uuid, size: blob.size, diff --git a/server/front/src/starter.ts b/server/front/src/starter.ts index f9a8fd5b1a..dde158120c 100644 --- a/server/front/src/starter.ts +++ b/server/front/src/starter.ts @@ -103,7 +103,7 @@ export function startFront (ctx: MeasureContext, extraConfig?: Record