feat: add uploads-resized route with sharp resizing and cache headers
This commit is contained in:
@@ -17,6 +17,7 @@ import { registerUserMessageRoutes } from './routes/user-messages.js'
|
||||
import { registerUserOrderRoutes } from './routes/user-orders.js'
|
||||
import { registerUserPaymentRoutes } from './routes/user-payments.js'
|
||||
import { registerOAuthSocialRoutes } from './routes/oauth-social.js'
|
||||
import { registerUploadsResized } from './routes/uploads-resized.js'
|
||||
|
||||
const port = Number(process.env.PORT) || 3333
|
||||
const origin = (process.env.CORS_ORIGIN ?? '')
|
||||
@@ -46,10 +47,19 @@ await fastify.register(multipart, {
|
||||
},
|
||||
})
|
||||
|
||||
registerUploadsResized(fastify)
|
||||
|
||||
const uploadsDir = path.join(process.cwd(), 'uploads')
|
||||
await fastify.register(fastifyStatic, {
|
||||
root: uploadsDir,
|
||||
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) {
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user