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 { 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) {
|
||||||
|
|||||||
@@ -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