Files
speckle-server/packages/server/modules/previews/index.js
T
Kristaps Fabians Geikins a9266333b4 fix(fe2): preview images being cached for too long (#2399)
* fix(fe2): preview images being cached for too long

* allowing caching of error previews

* updating cachebust value earlier
2024-06-19 16:26:35 +02:00

287 lines
8.2 KiB
JavaScript

/* istanbul ignore file */
'use strict'
const { validateScopes, authorizeResolver } = require('@/modules/shared')
const { getStream } = require('../core/services/streams')
const { getObject } = require('../core/services/objects')
const {
getCommitsByStreamId,
getCommitsByBranchName,
getCommitById
} = require('../core/services/commits')
const {
getPreviewImage,
createObjectPreview,
getObjectPreviewInfo
} = require('./services/previews')
const { makeOgImage } = require('./ogImage')
const { moduleLogger, logger } = require('@/logging/logging')
const {
listenForPreviewGenerationUpdates
} = require('@/modules/previews/services/resultListener')
const { Scopes, Roles } = require('@speckle/shared')
const httpErrorImage = (httpErrorCode) =>
require.resolve(`#/assets/previews/images/preview_${httpErrorCode}.png`)
const cors = require('cors')
const noPreviewImage = require.resolve('#/assets/previews/images/no_preview.png')
const previewErrorImage = require.resolve('#/assets/previews/images/preview_error.png')
exports.init = (app, isInitial) => {
if (process.env.DISABLE_PREVIEWS) {
moduleLogger.warn('📸 Object preview module is DISABLED')
} else {
moduleLogger.info('📸 Init object preview module')
}
const DEFAULT_ANGLE = '0'
const getObjectPreviewBufferOrFilepath = async ({ streamId, objectId, angle }) => {
if (process.env.DISABLE_PREVIEWS) {
return {
type: 'file',
file: noPreviewImage
}
}
// Check if objectId is valid
const dbObj = await getObject({ streamId, objectId })
if (!dbObj) {
return {
type: 'file',
file: require.resolve('#/assets/previews/images/preview_404.png'),
error: true,
errorCode: 'OBJECT_NOT_FOUND'
}
}
// Get existing preview metadata
const previewInfo = await getObjectPreviewInfo({ streamId, objectId })
if (!previewInfo) {
await createObjectPreview({ streamId, objectId, priority: 0 })
}
if (!previewInfo || previewInfo.previewStatus !== 2 || !previewInfo.preview) {
return { type: 'file', file: noPreviewImage }
}
const previewImgId = previewInfo.preview[angle]
if (!previewImgId) {
logger.warn(
`Preview angle '${angle}' not found for object ${streamId}:${objectId}`
)
return {
type: 'file',
error: true,
errorCode: 'ANGLE_NOT_FOUND',
file: previewErrorImage
}
}
const previewImg = await getPreviewImage({ previewId: previewImgId })
if (!previewImg) {
logger.warn(`Preview image not found: ${previewImgId}`)
return {
type: 'file',
file: previewErrorImage,
error: true,
errorCode: 'PREVIEW_NOT_FOUND'
}
}
return { type: 'buffer', buffer: previewImg }
}
const sendObjectPreview = async (req, res, streamId, objectId, angle) => {
let previewBufferOrFile = await getObjectPreviewBufferOrFilepath({
streamId,
objectId,
angle
})
if (req.query.postprocess === 'og') {
const stream = await getStream({ streamId: req.params.streamId })
const streamName = stream.name
if (previewBufferOrFile.type === 'file') {
previewBufferOrFile = {
type: 'buffer',
buffer: await makeOgImage(previewBufferOrFile.file, streamName)
}
} else {
previewBufferOrFile = {
type: 'buffer',
buffer: await 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)
}
}
const checkStreamPermissions = async (req) => {
const stream = await getStream({
streamId: req.params.streamId,
userId: req.context.userId
})
if (!stream) {
return { hasPermissions: false, httpErrorCode: 404 }
}
if (!stream.isPublic && req.context.auth === false) {
return { hasPermissions: false, httpErrorCode: 401 }
}
if (!stream.isPublic) {
try {
await validateScopes(req.context.scopes, Scopes.Streams.Read)
} catch {
return { hasPermissions: false, httpErrorCode: 401 }
}
try {
await authorizeResolver(
req.context.userId,
req.params.streamId,
Roles.Stream.Reviewer,
req.context.resourceAccessRules
)
} catch {
return { hasPermissions: false, httpErrorCode: 401 }
}
}
return { hasPermissions: true, httpErrorCode: 200 }
}
app.options('/preview/:streamId/:angle?', cors())
app.get('/preview/:streamId/:angle?', cors(), async (req, res) => {
const { hasPermissions, httpErrorCode } = await checkStreamPermissions(req)
if (!hasPermissions) {
// return res.status( httpErrorCode ).end()
return res.sendFile(httpErrorImage(httpErrorCode))
}
const { commits } = await getCommitsByStreamId({
streamId: req.params.streamId,
limit: 1,
ignoreGlobalsBranch: true
})
if (!commits || commits.length === 0) {
return res.sendFile(noPreviewImage)
}
const lastCommit = commits[0]
return sendObjectPreview(
req,
res,
req.params.streamId,
lastCommit.referencedObject,
req.params.angle || DEFAULT_ANGLE
)
})
app.options('/preview/:streamId/branches/:branchName/:angle?', cors())
app.get(
'/preview/:streamId/branches/:branchName/:angle?',
cors(),
async (req, res) => {
const { hasPermissions, httpErrorCode } = await checkStreamPermissions(req)
if (!hasPermissions) {
// return res.status( httpErrorCode ).end()
return res.sendFile(httpErrorImage(httpErrorCode))
}
let commitsObj
try {
commitsObj = await getCommitsByBranchName({
streamId: req.params.streamId,
branchName: req.params.branchName,
limit: 1
})
} catch {
commitsObj = {}
}
const { commits } = commitsObj
if (!commits || commits.length === 0) {
return res.sendFile(noPreviewImage)
}
const lastCommit = commits[0]
return sendObjectPreview(
req,
res,
req.params.streamId,
lastCommit.referencedObject,
req.params.angle || DEFAULT_ANGLE
)
}
)
app.options('/preview/:streamId/commits/:commitId/:angle?', cors())
app.get('/preview/:streamId/commits/:commitId/:angle?', cors(), async (req, res) => {
const { hasPermissions, httpErrorCode } = await checkStreamPermissions(req)
if (!hasPermissions) {
// return res.status( httpErrorCode ).end()
return res.sendFile(httpErrorImage(httpErrorCode))
}
const commit = await getCommitById({
streamId: req.params.streamId,
id: req.params.commitId
})
if (!commit) return res.sendFile(noPreviewImage)
return sendObjectPreview(
req,
res,
req.params.streamId,
commit.referencedObject,
req.params.angle || DEFAULT_ANGLE
)
})
app.options('/preview/:streamId/objects/:objectId/:angle?', cors())
app.get('/preview/:streamId/objects/:objectId/:angle?', cors(), async (req, res) => {
const { hasPermissions } = await checkStreamPermissions(req)
if (!hasPermissions) {
return res.status(403).end()
}
return sendObjectPreview(
req,
res,
req.params.streamId,
req.params.objectId,
req.params.angle || DEFAULT_ANGLE
)
})
if (isInitial) {
listenForPreviewGenerationUpdates()
}
}
exports.finalize = () => {}