Spaces:
Sleeping
Sleeping
| import { Jimp } from 'jimp' | |
| import path from 'path' | |
| import fs from 'fs/promises' | |
| import crypto from 'crypto' | |
| import { isAddonEnabled } from '../adminService' | |
| import { ADDON_IDS } from '../../addons' | |
| const THUMB_MAX = 800 | |
| const THUMB_QUALITY = 80 | |
| export async function ensureLocalThumbnail( | |
| uploadsRoot: string, | |
| originalRelPath: string, | |
| ): Promise<{ thumbnailRelPath: string; width: number; height: number } | null> { | |
| if (!isAddonEnabled(ADDON_IDS.JOURNEY)) return null | |
| const originalAbs = path.join(uploadsRoot, originalRelPath) | |
| try { await fs.access(originalAbs) } catch { return null } | |
| // Deterministic name so concurrent requests don't race on the same photo. | |
| const hash = crypto.createHash('sha1').update(originalRelPath).digest('hex').slice(0, 16) | |
| const thumbRel = `journey/thumbs/${hash}.jpg` | |
| const thumbAbs = path.join(uploadsRoot, thumbRel) | |
| try { | |
| const [srcStat, dstStat] = await Promise.all([ | |
| fs.stat(originalAbs), | |
| fs.stat(thumbAbs).catch(() => null), | |
| ]) | |
| if (dstStat && dstStat.mtimeMs >= srcStat.mtimeMs) { | |
| const img = await Jimp.read(thumbAbs) | |
| return { thumbnailRelPath: thumbRel, width: img.bitmap.width, height: img.bitmap.height } | |
| } | |
| await fs.mkdir(path.dirname(thumbAbs), { recursive: true }) | |
| // Jimp auto-applies EXIF orientation on read, matching sharp's .rotate() behavior. | |
| const img = await Jimp.read(originalAbs) | |
| const { width: w, height: h } = img.bitmap | |
| if (w > THUMB_MAX || h > THUMB_MAX) { | |
| img.scaleToFit({ w: THUMB_MAX, h: THUMB_MAX }) | |
| } | |
| await img.write(thumbAbs as `${string}.jpg`, { quality: THUMB_QUALITY }) | |
| return { thumbnailRelPath: thumbRel, width: img.bitmap.width, height: img.bitmap.height } | |
| } catch { | |
| // Unsupported format, corrupt file, etc. — fall back to original in caller. | |
| return null | |
| } | |
| } | |