feat: add image resize library with sharp
This commit is contained in:
@@ -0,0 +1,66 @@
|
|||||||
|
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 }
|
||||||
Reference in New Issue
Block a user