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>
This commit is contained in:
payacom
2026-09-15 17:55:09 +03:30
parent 26979057d7
commit e1abb40cc6
14 changed files with 316 additions and 27 deletions

View File

@@ -7,6 +7,13 @@ const ChatRoomModel = require('../../../models/ChatRoomModel')
const ChatRoomMessageModel = require('../../../models/ChatRoomMessageModel')
const UserModel = require('../../../models/UserModel')
const { ProvinceModel, CityModel } = require('../../../models/StateCity')
const { optimizeImageFile } = require('../../../utils/imageOptimize')
let optimizeVideoFaststart = async () => {}
try {
({ optimizeVideoFaststart } = require('../../../utils/postVideoProcessing'))
} catch (error) {
console.warn('[chatRoomController] postVideoProcessing unavailable:', error.message)
}
const {
slugifyTitle,
userCanSeeRoom,
@@ -227,6 +234,7 @@ const createRoom = async (req, res, next) => {
const uniqueFileName = `${Date.now()}-${crypto.randomUUID()}${path.extname(imageFile.name || '')}`
const filePath = path.join(uploadDir, uniqueFileName)
await fs.move(imageFile.path, filePath)
await optimizeImageFile(filePath, { maxDimension: 800 })
imagePath = `/chat-rooms/${uniqueFileName}`
}
@@ -397,6 +405,13 @@ const sendRoomMessage = async (req, res, next) => {
const uniqueFileName = `${Date.now()}-${crypto.randomUUID()}${path.extname(uploaded.name || '')}`
const filePath = path.join(uploadDir, uniqueFileName)
await fs.move(uploaded.path, filePath)
if (resolvedFileType === 'image' || (uploaded.type || '').startsWith('image/')) {
await optimizeImageFile(filePath)
} else if (resolvedFileType === 'video' || (uploaded.type || '').startsWith('video/')) {
optimizeVideoFaststart(filePath).catch((err) => {
console.error('[chatRoomController] video optimize background error:', err.message)
})
}
fileUrl = `/chat-rooms/messages/${uniqueFileName}`
}

View File

@@ -30,6 +30,7 @@ try {
} catch (error) {
console.warn('[postController] postVideoProcessing unavailable:', error.message)
}
const { optimizeImageFile } = require('../../../utils/imageOptimize')
const createPostValidationRules = () => {
return [
@@ -89,6 +90,14 @@ const createPost = async (req, res) => {
? maskPhoneNumbers(String(caption).trim().slice(0, 2000))
: null
// کوچک/فشرده‌کردنِ عکس‌ها قبل از ذخیره — قبل از اینکه مسیرشون توی دیتابیس بره، چون
// مسیر (file.path) دست‌نخورده می‌ماند، فقط بایت‌های خودِ فایل جایگزین می‌شوند
await Promise.all(
files
.filter((file) => (file.mimetype || file.type || '').startsWith('image/'))
.map((file) => optimizeImageFile(file.path))
)
const filePaths = files.map((file) => ({
path: file.path,
type: (file.mimetype || file.type || '').startsWith('video/') ? 'video' : 'image'

View File

@@ -6,6 +6,13 @@ const crypto = require('crypto')
const ShopModel = require('../../../models/ShopModel')
const ShopChatMessageModel = require('../../../models/ShopChatMessageModel')
const UserModel = require('../../../models/UserModel')
const { optimizeImageFile } = require('../../../utils/imageOptimize')
let optimizeVideoFaststart = async () => {}
try {
({ optimizeVideoFaststart } = require('../../../utils/postVideoProcessing'))
} catch (error) {
console.warn('[shopChatController] postVideoProcessing unavailable:', error.message)
}
function chatError (res, status, message) {
return res.status(status).json({ error: true, message })
@@ -235,6 +242,13 @@ const sendShopChatFileMessage = async (req, res, next) => {
const uniqueFileName = `${Date.now()}-${crypto.randomUUID()}${path.extname(file.name || '')}`
const filePath = path.join(uploadDir, uniqueFileName)
await fs.move(file.path, filePath, { overwrite: true })
if (fileType === 'image') {
await optimizeImageFile(filePath)
} else if (fileType === 'video') {
optimizeVideoFaststart(filePath).catch((err) => {
console.error('[shopChatController] video optimize background error:', err.message)
})
}
const fileUrl = `/shop-chat/${uniqueFileName}`
const message = await ShopChatMessageModel.create({

View File

@@ -19,6 +19,7 @@ const { enrichMessage, emitChatEvents } = require('../../../helpers/chatHelpers'
const { viewerIsBlockedBy } = require('../../../utils/blockVisibility')
const ShopCategoryModel = require('../../../models/ShopCategoryModel')
const { buildUnifiedInterestScore } = require('../../../utils/exploreAlgorithm')
const { optimizeImageFile } = require('../../../utils/imageOptimize')
/** Best-effort JWT decode for public browsing endpoints — never throws, never blocks guests. */
function getOptionalUserId (req) {
@@ -42,6 +43,9 @@ async function saveFile (file, folder, prefix) {
const uniqueFileName = `${prefix}-${Date.now()}${Math.random().toString().slice(2, 9)}${path.extname(file.name)}`
const filePath = path.join(uploadDir, uniqueFileName)
await fs.move(file.path, filePath, { overwrite: true })
if ((file.mimetype || file.type || '').startsWith('image/')) {
await optimizeImageFile(filePath)
}
return `/${folder}/${uniqueFileName}`
}

View File

@@ -8,6 +8,7 @@ const ShopRatingModel = require('../../../models/ShopRatingModel')
const ShopFollowModel = require('../../../models/ShopFollowModel')
const OrderModel = require('../../../models/OrderModel')
const { maskPhoneNumbers } = require('../../../utils/phoneNumberMasking')
const { optimizeImageFile } = require('../../../utils/imageOptimize')
const MS_DAY = 24 * 60 * 60 * 1000
const ANALYTICS_RANGE_DAYS = { '7d': 7, '30d': 30, '90d': 90, all: null }
@@ -286,6 +287,7 @@ const updateShopLogo = async (req, res, next) => {
const uniqueFileName = `${shop._id}-${Date.now()}${path.extname(logo.name)}`
const filePath = path.join(uploadDir, uniqueFileName)
await fs.move(logo.path, filePath, { overwrite: true })
await optimizeImageFile(filePath, { maxDimension: 800 })
shop.logo = `/shops/${uniqueFileName}`
await shop.save()

View File

@@ -13,6 +13,17 @@ const { filterStoryUserIdsByPrivateProfile } = require('../../../utils/privatePr
const { sendStoryReactionMessage } = require('../../../utils/storyChatMessage')
const { enrichMessage, emitChatEvents } = require('../../../helpers/chatHelpers')
const { resolveLinkedEntity } = require('../../../utils/resolveLinkedEntity')
const { optimizeImageFile } = require('../../../utils/imageOptimize')
let optimizeVideoFaststart = async () => {}
try {
({ optimizeVideoFaststart } = require('../../../utils/postVideoProcessing'))
} catch (error) {
console.warn('[storyController] postVideoProcessing unavailable:', error.message)
}
// استوری روی صفحه‌ی کامل موبایل دیده می‌شه، ولی پهنا/ارتفاعِ خامِ دوربین (اغلب بیشتر از
// عرضِ صفحه‌ست) لازم نیست دست‌نخورده بمونه
const STORY_MAX_DIMENSION = 1440
const { STORY_PHOTO_FILTER_NAMES, applyStoryPhotoFilter } = require('../../../utils/storyPhotoFilters')
const LINK_OVERLAY_KINDS = ['pkg', 'project', 'billboard', 'shop', 'shopproduct', 'booking']
@@ -212,6 +223,13 @@ const createStoryBase64 = async (req, res) => {
const filename = `${Date.now()}-${userId}${ext}`
const filePath = path.join(storyStorageDir, filename)
await fs.promises.writeFile(filePath, buffer)
if (mediaType === 'image') {
await optimizeImageFile(filePath, { maxDimension: STORY_MAX_DIMENSION })
} else {
optimizeVideoFaststart(filePath).catch((err) => {
console.error('[storyController] video optimize background error:', err.message)
})
}
const story = await StoryModel.create({
user_id: userId,
@@ -243,6 +261,13 @@ const createStoryMultipart = async (req, res) => {
}
const mediaType = (file.mimetype || '').startsWith('video/') ? 'video' : 'image'
if (mediaType === 'image') {
await optimizeImageFile(file.path, { maxDimension: STORY_MAX_DIMENSION })
} else {
optimizeVideoFaststart(file.path).catch((err) => {
console.error('[storyController] video optimize background error:', err.message)
})
}
let overlays = []
try {

View File

@@ -3,6 +3,11 @@ const fs = require('fs-extra')
const path = require('path')
const UserModel = require('../../../models/UserModel')
const jwt = require('jsonwebtoken')
const { optimizeImageFile } = require('../../../utils/imageOptimize')
// آواتار هیچ‌وقت بزرگ‌تر از این نمایش داده نمی‌شود — عکسِ خامِ دوربین (چند مگابایت) رو
// برای یه دایره‌ی کوچیک نگه‌داشتن فقط مصرفِ اینترنت رو بالا می‌بره
const AVATAR_MAX_DIMENSION = 800
const setProfileImage = async (req, res, next) => {
try {
@@ -34,6 +39,7 @@ const setProfileImage = async (req, res, next) => {
const filePath = path.join(uploadDir, uniqueFileName)
await fs.move(profile_image.path, filePath)
await optimizeImageFile(filePath, { maxDimension: AVATAR_MAX_DIMENSION })
// ذخیره مسیر فایل در دیتابیس
user.profile_image = `/profiles/${uniqueFileName}`
await user.save()
@@ -81,6 +87,7 @@ const updateProfileImage = async (req, res, next) => {
const filePath = path.join(uploadDir, uniqueFileName)
await fs.move(profile_image.path, filePath)
await optimizeImageFile(filePath, { maxDimension: AVATAR_MAX_DIMENSION })
user.profile_image = `/profiles/${uniqueFileName}`
await user.save()