import crypto from 'node:crypto' import fs from 'node:fs' import path from 'node:path' const UPLOADS_DIR = path.join(process.cwd(), 'uploads') const CACHE_DIR = path.join(UPLOADS_DIR, '.cache') const VALID_WIDTHS = [320, 640, 1024, 1600] const SUPPORTED_FORMATS = new Set(['avif', 'webp']) /** * Find the original file by UUID (without extension) in the uploads directory tree. * Searches both /uploads/ and /uploads/reviews/. * Returns full path or null. */ export async function findOriginalFile(uuid, subdir = '') { const searchDirs = subdir ? [subdir] : ['', 'reviews'] for (const dir of searchDirs) { for (const ext of ['.png', '.jpg', '.jpeg', '.webp']) { const fullPath = path.join(UPLOADS_DIR, dir, `${uuid}${ext}`) try { await fs.promises.access(fullPath) return fullPath } catch { // file not found with this extension, try next } } } return null } /** * Get or generate a resized image. Returns { path: string, isNew: boolean }. */ export async function getOrCreateResized(uuid, width, format, subdir = '') { const cacheSubdir = subdir ? subdir : '' const cacheFileName = `${uuid}_w${width}.${format}` const cachePath = path.join(CACHE_DIR, cacheSubdir, cacheFileName) try { await fs.promises.access(cachePath) return { path: cachePath, isNew: false } } catch { // cache miss, need to generate } const originalPath = await findOriginalFile(uuid, subdir) if (!originalPath) { return null } await fs.promises.mkdir(path.dirname(cachePath), { recursive: true }) const sharp = (await import('sharp')).default let pipeline = sharp(originalPath) if (width) { pipeline = pipeline.resize(width, null, { withoutEnlargement: true }) } const options = format === 'avif' ? { quality: 75, effort: 4 } : { quality: 80 } await pipeline[format](options).toFile(cachePath) return { path: cachePath, isNew: true } } export { VALID_WIDTHS, SUPPORTED_FORMATS }