Fix exit code 132 on old CPUs by replacing sharp with jimp (issue #888) (#895)

sharp's prebuilt Linux x64 binary requires SSE4.2 (x86-64-v2), causing a
SIGILL crash on older hardware (e.g. AMD A6-3420M). Replace with jimp, a
pure-JS image library with no native binaries. Also skip thumbnail generation
entirely when the Journey addon is disabled (the default), preventing the
issue for most installs regardless of the image library used.
This commit is contained in:
Julien G.
2026-04-26 13:26:09 +02:00
committed by GitHub
parent cb425fb397
commit bc1fb71391
4 changed files with 908 additions and 542 deletions
+886 -527
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -23,6 +23,7 @@
"express": "^4.18.3",
"fast-xml-parser": "^5.5.10",
"helmet": "^8.1.0",
"jimp": "^1.6.1",
"jsonwebtoken": "^9.0.2",
"multer": "^2.1.1",
"node-cron": "^4.2.1",
@@ -30,7 +31,6 @@
"otplib": "^12.0.1",
"qrcode": "^1.5.4",
"semver": "^7.7.4",
"sharp": "^0.34.5",
"tsx": "^4.21.0",
"typescript": "^6.0.2",
"undici": "^7.0.0",
@@ -1,7 +1,9 @@
import sharp from 'sharp'
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
@@ -10,12 +12,14 @@ 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}.webp`
const thumbRel = `journey/thumbs/${hash}.jpg`
const thumbAbs = path.join(uploadsRoot, thumbRel)
try {
@@ -24,18 +28,21 @@ export async function ensureLocalThumbnail(
fs.stat(thumbAbs).catch(() => null),
])
if (dstStat && dstStat.mtimeMs >= srcStat.mtimeMs) {
const meta = await sharp(thumbAbs).metadata()
return { thumbnailRelPath: thumbRel, width: meta.width ?? 0, height: meta.height ?? 0 }
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 })
await sharp(originalAbs)
.rotate()
.resize({ width: THUMB_MAX, height: THUMB_MAX, fit: 'inside', withoutEnlargement: true })
.webp({ quality: THUMB_QUALITY })
.toFile(thumbAbs)
const meta = await sharp(thumbAbs).metadata()
return { thumbnailRelPath: thumbRel, width: meta.width ?? 0, height: meta.height ?? 0 }
// 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