Files
modstagram-back/utils/imageOptimize.js
payacom e1abb40cc6 Reduce media bandwidth: compress uploads, add long-lived cache headers, gzip API
- Resize/re-encode every uploaded image server-side (posts, avatars, stories,
  shop products/logo, chat messages) via a shared sharp-based optimizer —
  caps dimensions to what's actually displayed and re-compresses in place,
  cutting a typical raw camera photo by ~90% with no visible quality loss
- Cap uploaded video resolution/bitrate (re-encode only when above 1280px/
  ~2.5Mbps) alongside the existing faststart step; apply faststart+cap to
  story and chat videos too (previously stored completely raw)
- Add long-lived Cache-Control to all /storage static routes so clients stop
  re-downloading unchanged media on every request
- Add gzip/brotli compression for JSON API responses (feed/explore/chat)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 17:55:09 +03:30

60 lines
3.1 KiB
JavaScript

const sharp = require('sharp')
const fs = require('fs-extra')
const path = require('path')
// اندازه/کیفیتِ نمایش — نه سایزِ چاپ؛ کاربر روی موبایل/وب حداکثر تا این عرض می‌بینه،
// پس آپلودِ خامِ دوربین (معمولاً ۳۰۰۰-۴۰۰۰ پیکسل) فقط مصرفِ اینترنت رو چند برابر می‌کنه
// بدون هیچ بهبودِ بصری. این مقادیر برای محتوای پست/استوری تنظیم شده‌اند.
const DEFAULT_MAX_DIMENSION = 1600
const DEFAULT_JPEG_QUALITY = 82
/**
* تصویرِ رویِ دیسک را (در صورت لازم) کوچک و فشرده می‌کند — همون فایل با همون
* پسوند/فرمت بازنویسی می‌شود تا Content-Type و مسیرهای ذخیره‌شده در دیتابیس تغییر نکنند.
* اگر فایلِ فشرده‌شده کوچک‌تر نبود (مثلاً عکسِ کوچک/از قبل فشرده)، فایلِ اصلی نگه داشته می‌شود.
*/
async function optimizeImageFile(filePath, { maxDimension = DEFAULT_MAX_DIMENSION, jpegQuality = DEFAULT_JPEG_QUALITY } = {}) {
if (!filePath) return
let tempPath = null
try {
if (!(await fs.pathExists(filePath))) return
const ext = path.extname(filePath).toLowerCase()
const metadata = await sharp(filePath, { failOn: 'none' }).metadata()
if (!metadata.width || !metadata.height) return // فرمتِ ناشناس برای sharp (مثلاً gif متحرک) — دست‌نخورده می‌ماند
const needsResize = metadata.width > maxDimension || metadata.height > maxDimension
let pipeline = sharp(filePath, { failOn: 'none' }).rotate() // rotate() بدون آرگومان: EXIF orientation را اعمال و حذف می‌کند
if (needsResize) {
pipeline = pipeline.resize({ width: maxDimension, height: maxDimension, fit: 'inside', withoutEnlargement: true })
}
if (ext === '.png') {
pipeline = pipeline.png({ compressionLevel: 9, palette: true })
} else if (ext === '.webp') {
pipeline = pipeline.webp({ quality: jpegQuality })
} else {
// .jpg/.jpeg و هر پسوندِ دیگرِ عکس (heic و مشابه، اگر sharp بتواند دیکد کند)
pipeline = pipeline.jpeg({ quality: jpegQuality, mozjpeg: true })
}
tempPath = path.join(path.dirname(filePath), `optimize-${Date.now()}-${path.basename(filePath)}`)
await pipeline.toFile(tempPath)
const [originalStat, optimizedStat] = await Promise.all([fs.stat(filePath), fs.stat(tempPath)])
if (optimizedStat.size >= originalStat.size) {
await fs.remove(tempPath)
return
}
await fs.rename(tempPath, filePath)
console.log(`[imageOptimize] ${path.basename(filePath)}: ${originalStat.size} -> ${optimizedStat.size} bytes`)
} catch (error) {
console.error('[imageOptimize] failed:', error.message)
if (tempPath) await fs.remove(tempPath).catch(() => {})
}
}
module.exports = { optimizeImageFile, DEFAULT_MAX_DIMENSION, DEFAULT_JPEG_QUALITY }