You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wiki/backend/controllers/thumb.ts

46 lines
1.6 KiB

import crypto from 'node:crypto'
import { validate as uuidValidate } from 'uuid'
import type { FastifyInstance } from 'fastify'
/**
* A thumbnail is generated once, at upload time, and an asset that changes gets a new ID — so the
* bytes behind a given URL never change.
*/
const THUMB_CACHE = 'public, max-age=31536000, immutable'
/**
* _thumb Routes
*
* Public, like `_site` and `_user`: a thumbnail is a shrunken copy of an asset already served on the
* pages that embed it, and the URL has to be known to be asked for. Only assets that have a preview
* answer here — everything else, including every non-image, is a 404 the file manager draws a file
* type icon for.
*/
async function routes(app: FastifyInstance) {
app.get<{ Params: { fileName: string } }>('/:fileName', async (req, reply) => {
// -> `.webp` is part of the URL so that the extension matches what is served, but the ID is the
// only part that identifies anything
const assetId = req.params.fileName.replace(/\.webp$/i, '')
if (!uuidValidate(assetId)) {
return reply.notFound('Thumbnail not found')
}
const preview = await WIKI.models.assets.getThumbnail(assetId)
if (!preview) {
return reply.notFound('Thumbnail not found')
}
const etag = `"${crypto.createHash('sha1').update(preview).digest('hex')}"`
reply.header('ETag', etag)
reply.header('Cache-Control', THUMB_CACHE)
reply.header('X-Content-Type-Options', 'nosniff')
if (req.headers['if-none-match'] === etag) {
return reply.code(304).send()
}
return reply.type('image/webp').send(preview)
})
}
export default routes