UBERF-7597: Get rid of formats in preview.ts (#6077)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-07-16 16:39:30 +07:00 committed by GitHub
parent 7fb8737959
commit d6eaaa9f01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 56 additions and 49 deletions

View File

@ -4,9 +4,6 @@ import { getMetadata } from '@hcengineering/platform'
import { getBlobHref, getClient, getCurrentWorkspaceUrl, getFileUrl } from '.' import { getBlobHref, getClient, getCurrentWorkspaceUrl, getFileUrl } from '.'
import presentation from './plugin' import presentation from './plugin'
type SupportedFormat = string
const defaultSupportedFormats = 'avif,webp,heif,jpeg'
export interface ProviderPreviewConfig { export interface ProviderPreviewConfig {
// Identifier of provider // 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. // 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 // Preview url
// If '' preview is disabled for config. // If '' preview is disabled for config.
previewUrl: string 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. // Content type markers, will check by containts, if passed, only allow to be used with matched content types.
contentTypes?: string[] contentTypes?: string[]
@ -28,8 +23,7 @@ export interface PreviewConfig {
const defaultPreview = (): ProviderPreviewConfig => ({ const defaultPreview = (): ProviderPreviewConfig => ({
providerId: '', providerId: '',
formats: ['avif', 'webp', 'jpg'], previewUrl: `/files/${getCurrentWorkspaceUrl()}?file=:blobId&size=:size`
previewUrl: `/files/${getCurrentWorkspaceUrl()}?file=:blobId.:format&size=:size`
}) })
/** /**
@ -39,7 +33,7 @@ const defaultPreview = (): ProviderPreviewConfig => ({
- providerName - a provider name should be same as in Storage configuration. - providerName - a provider name should be same as in Storage configuration.
It coult be empty and it will match by content types. 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. - supportedFormats - a `,` separated list of file extensions.
- contentTypes - a ',' separated list of content type patterns. - contentTypes - a ',' separated list of content type patterns.
@ -58,14 +52,14 @@ export function parsePreviewConfig (config?: string): PreviewConfig | undefined
if (c === '') { if (c === '') {
continue // Skip empty lines continue // Skip empty lines
} }
const vars = c.split('|')
let [provider, url, formats, contentTypes] = c.split('|').map((it) => it.trim()) let [provider, url, formats, contentTypes] = c.split('|').map((it) => it.trim())
if (formats === undefined) { if (vars.length === 3) {
formats = defaultSupportedFormats contentTypes = formats // Backward compatibility, since formats are obsolete
} }
const p: ProviderPreviewConfig = { const p: ProviderPreviewConfig = {
providerId: provider, providerId: provider,
previewUrl: url, previewUrl: url,
formats: formats.split(',').map((it) => it.trim()),
// Allow preview only for images by default // Allow preview only for images by default
contentTypes: contentTypes:
contentTypes !== undefined contentTypes !== undefined
@ -94,7 +88,6 @@ export function getPreviewConfig (): PreviewConfig {
{ {
providerId: '', providerId: '',
contentTypes: ['image/gif', 'image/apng', 'image/svg'], // Disable gif and apng format preview. contentTypes: ['image/gif', 'image/apng', 'image/svg'], // Disable gif and apng format preview.
formats: [],
previewUrl: '' previewUrl: ''
} }
] ]
@ -182,13 +175,7 @@ function blobToSrcSet (
url = url.replaceAll(':blobId', encodeURIComponent(blob._id)) url = url.replaceAll(':blobId', encodeURIComponent(blob._id))
let result = '' let result = ''
for (const f of cfg.formats ?? []) { const fu = url
if (result.length > 0) {
result += ', '
}
const fu = url.replaceAll(':format', f)
if (width !== undefined) { if (width !== undefined) {
result += result +=
fu.replaceAll(':size', `${width}`) + fu.replaceAll(':size', `${width}`) +
@ -200,7 +187,7 @@ function blobToSrcSet (
} else { } else {
result += fu.replaceAll(':size', `${-1}`) result += fu.replaceAll(':size', `${-1}`)
} }
}
return result return result
} }

View File

@ -28,11 +28,11 @@ A `;` separated list of triples, providerName|previewUrl|supportedFormats.
- providerName - a provider name should be same as in Storage configuration. - providerName - a provider name should be same as in Storage configuration.
It coult be empty and it will match by content types. 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. - supportedFormats - a `,` separated list of file extensions.
- contentTypes - a ',' separated list of content type patterns. - 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 ## Variables
@ -40,7 +40,6 @@ PREVIEW_CONFIG=*|https://front.hc.engineering/files/:workspace/api/preview/?form
- :blobId - an uniq blob _id identifier. - :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. - :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. - :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. ## 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. 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. ## Testing with dev-production/etc.

View File

@ -32,13 +32,11 @@ import { v4 as uuid } from 'uuid'
import { preConditions } from './utils' import { preConditions } from './utils'
import fs from 'fs' import fs from 'fs'
import { Readable } from 'stream'
const cacheControlValue = 'public, max-age=365d' const cacheControlValue = 'public, max-age=365d'
const cacheControlNoCache = 'public, no-store, no-cache, must-revalidate, max-age=0' 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 ( async function storageUpload (
ctx: MeasureContext, ctx: MeasureContext,
storageAdapter: StorageAdapter, storageAdapter: StorageAdapter,
@ -118,6 +116,8 @@ async function getFileRange (
'Last-Modified': new Date(stat.modifiedOn).toISOString() 'Last-Modified': new Date(stat.modifiedOn).toISOString()
}) })
res.send(Readable.toWeb(dataStream))
dataStream.pipe(res) dataStream.pipe(res)
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
@ -179,7 +179,12 @@ async function getFile (
} }
if (preConditions.IfUnmodifiedSince(req.headers, { lastModified: new Date(stat.modifiedOn) }) === 'failed') { if (preConditions.IfUnmodifiedSince(req.headers, { lastModified: new Date(stat.modifiedOn) }) === 'failed') {
// Send 412 (Precondition 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() res.end()
return return
} }
@ -360,17 +365,12 @@ export function start (
(req.query.token as string | undefined) (req.query.token as string | undefined)
payload = token !== undefined ? decodeToken(token) : payload 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) { if (uuid === undefined) {
res.status(404).send() res.status(404).send()
return 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( let blobInfo = await ctx.with(
'notoken-stat', 'notoken-stat',
{ workspace: payload.workspace.name }, { workspace: payload.workspace.name },
@ -411,12 +411,13 @@ export function start (
} }
const size = req.query.size !== undefined ? parseInt(req.query.size as string) : undefined 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( blobInfo = await ctx.with(
'resize', 'resize',
{}, {},
async (ctx) => 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 ( async function getGeneratePreview (
ctx: MeasureContext, ctx: MeasureContext,
blob: PlatformBlob, blob: PlatformBlob,
@ -738,11 +741,29 @@ async function getGeneratePreview (
uuid: string, uuid: string,
config: { storageAdapter: StorageAdapter }, config: { storageAdapter: StorageAdapter },
payload: Token, payload: Token,
format: SupportedFormat = 'jpeg' accept: string
): Promise<PlatformBlob> { ): Promise<PlatformBlob> {
if (size === undefined) { if (size === undefined) {
return blob 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 sizeId = uuid + `%preview%${size}${format !== 'jpeg' ? format : ''}`
const d = await config.storageAdapter.stat(ctx, payload.workspace, sizeId) const d = await config.storageAdapter.stat(ctx, payload.workspace, sizeId)
@ -823,7 +844,7 @@ async function getGeneratePreview (
Analytics.handleError(err) Analytics.handleError(err)
ctx.error('failed to resize image', { ctx.error('failed to resize image', {
err, err,
format, format: accept,
contentType: blob.contentType, contentType: blob.contentType,
uuid, uuid,
size: blob.size, size: blob.size,

View File

@ -103,7 +103,7 @@ export function startFront (ctx: MeasureContext, extraConfig?: Record<string, st
if (previewConfig === undefined) { if (previewConfig === undefined) {
// Use universal preview config // Use universal preview config
previewConfig = `*|${uploadUrl}/:workspace?file=:blobId.:format&size=:size` previewConfig = `*|${uploadUrl}/:workspace?file=:blobId&size=:size`
} }
const pushPublicKey = process.env.PUSH_PUBLIC_KEY const pushPublicKey = process.env.PUSH_PUBLIC_KEY