Files
speckle-server/packages/server/modules/previews/services/management.ts
T
Kristaps Fabians Geikins bde148f286 chore(server): migrating fully to ESM (#5042)
* wip

* some extra fixes

* stuff kinda works?

* need to figure out mocks

* need to figure out mocks

* fix db listener

* gqlgen fix

* minor gqlgen watch adjustment

* lint fixes

* delete old codegen file

* converting migrations to ESM

* getModuleDIrectory

* vitest sort of works

* added back ts-vitest

* resolve gql double load

* fixing test timeout configs

* TSC lint fix

* fix automate tests

* moar debugging

* debugging

* more debugging

* codegen update

* server works

* yargs migrated

* chore(server): getting rid of global mocks for Server ESM (#5046)

* got rid of email mock

* got rid of comment mocks

* got rid of multi region mocks

* got rid of stripe mock

* admin override mock updated

* removed final mock

* fixing import.meta.resolve calls

* another import.meta.resolve fix

* added requested test

* nyc ESM fix

* removed unneeded deps + linting

* yarn lock forgot to commit

* tryna fix flakyness

* email capture util fix

* sendEmail fix

* fix TSX check

* sender transporter fix + CR comments

* merge main fix

* test fixx

* circleci fix

* gqlgen bigint fix

* error formatter fix

* more error formatting improvements

* esmloader added to Dockerfile

* more dockerfile fixes

* bg jobs fix
2025-07-14 10:26:19 +03:00

201 lines
6.2 KiB
TypeScript

import type { GetFormattedObject } from '@/modules/core/domain/objects/operations'
import type { GetStream } from '@/modules/core/domain/streams/operations'
import type {
CheckStreamPermissions,
CreateObjectPreview,
GetObjectPreviewBufferOrFilepath,
GetObjectPreviewInfo,
GetPreviewImage,
SendObjectPreview
} from '@/modules/previews/domain/operations'
import { makeOgImage } from '@/modules/previews/ogImage'
import { authorizeResolver, validateScopes } from '@/modules/shared'
import { disablePreviews } from '@/modules/shared/helpers/envHelper'
import { Roles, Scopes } from '@speckle/shared'
import type { Logger } from 'pino'
import { PreviewPriority, PreviewStatus } from '@/modules/previews/domain/consts'
import { ProjectRecordVisibility } from '@/modules/core/helpers/types'
import { fileURLToPath } from 'url'
const defaultAngle = '0'
export const getObjectPreviewBufferOrFilepathFactory =
(deps: {
getObject: GetFormattedObject
getObjectPreviewInfo: GetObjectPreviewInfo
createObjectPreview: CreateObjectPreview
getPreviewImage: GetPreviewImage
logger: Logger
}): GetObjectPreviewBufferOrFilepath =>
async ({ streamId, objectId, angle }) => {
const [noPreviewImage, previewErrorImage] = await Promise.all([
fileURLToPath(import.meta.resolve('#/assets/previews/images/no_preview.png')),
fileURLToPath(import.meta.resolve('#/assets/previews/images/preview_error.png'))
])
angle = angle || defaultAngle
const boundLogger = deps.logger.child({ streamId, objectId, angle })
if (disablePreviews()) {
return {
type: 'file',
file: noPreviewImage
}
}
// Check if objectId is valid
const dbObj = await deps.getObject({ streamId, objectId })
if (!dbObj) {
return {
type: 'file',
file: fileURLToPath(
import.meta.resolve('#/assets/previews/images/preview_404.png')
),
error: true,
errorCode: 'OBJECT_NOT_FOUND'
}
}
// Get existing preview metadata
const previewInfo = await deps.getObjectPreviewInfo({ streamId, objectId })
if (!previewInfo) {
const objPreviewQueued = await deps.createObjectPreview({
streamId,
objectId,
priority: PreviewPriority.LOW
})
if (!objPreviewQueued) return { type: 'file', file: noPreviewImage }
}
if (
!previewInfo ||
previewInfo.previewStatus !== PreviewStatus.DONE ||
!previewInfo.preview
) {
return { type: 'file', file: noPreviewImage }
}
const previewImgId = previewInfo.preview[angle]
if (!previewImgId) {
boundLogger.warn(
"Preview angle '{angle}' not found for object {streamId}:{objectId}"
)
return {
type: 'file',
error: true,
errorCode: 'ANGLE_NOT_FOUND',
file: previewErrorImage
}
}
const previewImg = await deps.getPreviewImage({ previewId: previewImgId })
if (!previewImg) {
boundLogger.warn(
{ previewImageId: previewImgId },
'Preview image not found: {previewImageId}'
)
return {
type: 'file',
file: previewErrorImage,
error: true,
errorCode: 'PREVIEW_NOT_FOUND'
}
}
return { type: 'buffer', buffer: previewImg }
}
export const sendObjectPreviewFactory =
(deps: {
getObjectPreviewBufferOrFilepath: GetObjectPreviewBufferOrFilepath
getStream: GetStream
makeOgImage: typeof makeOgImage
}): SendObjectPreview =>
async (req, res, streamId, objectId, angle) => {
let previewBufferOrFile = await deps.getObjectPreviewBufferOrFilepath({
streamId,
objectId,
angle
})
if (req.query.postprocess === 'og') {
const stream = await deps.getStream({ streamId: req.params.streamId })
const streamName = stream!.name
if (previewBufferOrFile.type === 'file') {
previewBufferOrFile = {
type: 'buffer',
buffer: await deps.makeOgImage(previewBufferOrFile.file, streamName)
}
} else {
previewBufferOrFile = {
type: 'buffer',
buffer: await deps.makeOgImage(previewBufferOrFile.buffer, streamName)
}
}
}
if (previewBufferOrFile.error) {
res.set('X-Preview-Error', 'true')
}
if (previewBufferOrFile.errorCode) {
res.set('X-Preview-Error-Code', previewBufferOrFile.errorCode)
}
if (previewBufferOrFile.type === 'file') {
// we can't cache these cause they may switch to proper buffer previews in a sec
// at least if they're not in the error state which they will not get out of (and thus can be cached in that scenario)
if (previewBufferOrFile.error) {
res.set('Cache-Control', 'private, max-age=604800')
} else {
res.set('Cache-Control', 'no-cache, no-store')
}
res.sendFile(previewBufferOrFile.file)
} else {
res.contentType('image/png')
// If the preview is a buffer, it comes from the DB and can be cached on clients
res.set('Cache-Control', 'private, max-age=604800')
res.send(previewBufferOrFile.buffer)
}
}
export const checkStreamPermissionsFactory =
(deps: {
validateScopes: typeof validateScopes
authorizeResolver: typeof authorizeResolver
getStream: GetStream
}): CheckStreamPermissions =>
async (req) => {
const stream = await deps.getStream({
streamId: req.params.streamId,
userId: req.context.userId
})
if (!stream) {
return { hasPermissions: false, httpErrorCode: 404 }
}
if (
stream.visibility !== ProjectRecordVisibility.Public &&
req.context.auth === false
) {
return { hasPermissions: false, httpErrorCode: 401 }
}
if (stream.visibility !== ProjectRecordVisibility.Public) {
try {
await deps.validateScopes(req.context.scopes, Scopes.Streams.Read)
} catch {
return { hasPermissions: false, httpErrorCode: 401 }
}
try {
await deps.authorizeResolver(
req.context.userId,
req.params.streamId,
Roles.Stream.Reviewer,
req.context.resourceAccessRules
)
} catch {
return { hasPermissions: false, httpErrorCode: 401 }
}
}
return { hasPermissions: true, httpErrorCode: 200 }
}