feat: add uploads-resized route with sharp resizing and cache headers

This commit is contained in:
Kirill
2026-05-15 13:24:16 +05:00
parent c37743eee6
commit 0bef02bc6d
2 changed files with 82 additions and 0 deletions
+10
View File
@@ -17,6 +17,7 @@ import { registerUserMessageRoutes } from './routes/user-messages.js'
import { registerUserOrderRoutes } from './routes/user-orders.js' import { registerUserOrderRoutes } from './routes/user-orders.js'
import { registerUserPaymentRoutes } from './routes/user-payments.js' import { registerUserPaymentRoutes } from './routes/user-payments.js'
import { registerOAuthSocialRoutes } from './routes/oauth-social.js' import { registerOAuthSocialRoutes } from './routes/oauth-social.js'
import { registerUploadsResized } from './routes/uploads-resized.js'
const port = Number(process.env.PORT) || 3333 const port = Number(process.env.PORT) || 3333
const origin = (process.env.CORS_ORIGIN ?? '') const origin = (process.env.CORS_ORIGIN ?? '')
@@ -46,10 +47,19 @@ await fastify.register(multipart, {
}, },
}) })
registerUploadsResized(fastify)
const uploadsDir = path.join(process.cwd(), 'uploads') const uploadsDir = path.join(process.cwd(), 'uploads')
await fastify.register(fastifyStatic, { await fastify.register(fastifyStatic, {
root: uploadsDir, root: uploadsDir,
prefix: '/uploads/', prefix: '/uploads/',
setHeaders(res, filePath) {
if (filePath.includes('/.cache/')) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
} else {
res.setHeader('Cache-Control', 'public, max-age=86400')
}
},
}) })
fastify.decorate('authenticate', async function authenticate(request, reply) { fastify.decorate('authenticate', async function authenticate(request, reply) {
+72
View File
@@ -0,0 +1,72 @@
// server/src/routes/uploads-resized.js
import fs from 'node:fs'
import path from 'node:path'
import { findOriginalFile, getOrCreateResized, SUPPORTED_FORMATS, VALID_WIDTHS } from '../lib/image-resize.js'
const CACHE_CONTROL_IMMUTABLE = 'public, max-age=31536000, immutable'
const CACHE_CONTROL_SHORT = 'public, max-age=86400'
/**
* Register GET /uploads-resized/* route for on-demand image resizing.
* Must be registered BEFORE fastify-static for /uploads/.
*/
export function registerUploadsResized(fastify) {
fastify.get('/uploads-resized/*', async (request, reply) => {
const rawPath = request.params['*']
const url = new URL(request.url, 'http://localhost')
const widthParam = url.searchParams.get('w')
// Parse: [subdir/]filename.format
const parts = rawPath.split('/')
let filename, subdir = ''
if (parts.length > 1) {
subdir = parts.slice(0, -1).join('/') + '/'
filename = parts[parts.length - 1]
} else {
filename = parts[0]
}
const dotIdx = filename.lastIndexOf('.')
if (dotIdx === -1) {
return reply.code(400).send({ error: 'Invalid request: no format specified' })
}
const uuid = filename.slice(0, dotIdx)
const format = filename.slice(dotIdx + 1).toLowerCase()
if (!SUPPORTED_FORMATS.has(format)) {
return reply.code(400).send({ error: `Unsupported format: ${format}. Use avif or webp.` })
}
// Validate width
let width = null
if (widthParam) {
const w = parseInt(widthParam, 10)
if (!VALID_WIDTHS.includes(w)) {
return reply.code(400).send({ error: `Invalid width: ${widthParam}. Use: ${VALID_WIDTHS.join(', ')}` })
}
width = w
}
// If no width requested, serve original with short cache
if (!width) {
const originalPath = await findOriginalFile(uuid, subdir || undefined)
if (!originalPath) {
return reply.code(404).send({ error: 'Image not found' })
}
reply.header('Cache-Control', CACHE_CONTROL_SHORT)
reply.header('Content-Type', format === 'avif' ? 'image/avif' : 'image/webp')
return reply.send(fs.createReadStream(originalPath))
}
const result = await getOrCreateResized(uuid, width, format, subdir || undefined)
if (!result) {
return reply.code(404).send({ error: 'Image not found' })
}
reply.header('Cache-Control', CACHE_CONTROL_IMMUTABLE)
reply.header('Content-Type', format === 'avif' ? 'image/avif' : 'image/webp')
return reply.send(fs.createReadStream(result.path))
})
}