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

@@ -1,8 +1,16 @@
require('dotenv').config();
const express = require('express');
const app = express();
const compression = require('compression');
const path = require('path');
const crypto = require('crypto');
const { optimizeImageFile } = require('./utils/imageOptimize');
let optimizeVideoFaststart = async () => {};
try {
({ optimizeVideoFaststart } = require('./utils/postVideoProcessing'));
} catch (error) {
console.warn('[index] postVideoProcessing unavailable:', error.message);
}
const http = require('http').createServer(app);
const cors = require('cors');
const jwt = require('jsonwebtoken');
@@ -106,30 +114,41 @@ const io = require('socket.io')(http, {
app.set('io', io);
// gzip/brotli فقط برای پاسخ‌های متنی (JSON فید/اکسپلور/چت) — فیلترِ پیش‌فرضِ compression
// خودش مدیا (image/*، video/*) رو compressible نمی‌دونه و رد می‌کنه، پس نیازی به فیلترِ
// دستی نیست؛ فقط باید قبل از هر res.json/res.send دیگه‌ای بیاد
app.use(compression());
// افزایش limit برای Base64 (1000mb کافیه)
app.use(express.json({ limit: '1000mb' }));
app.use(express.urlencoded({ limit: '1000mb', extended: true }));
// کش طولانی برای مدیا — بدون این، مرورگر/اپ هر بار همون عکس/ویدیوی قبلی رو دوباره
// دانلود می‌کرد. immutable نمی‌ذاریم چون مسیر آپلود تازه (optimizeImageFile/
// optimizeVideoFaststart) گاهی چند ثانیه بعدِ همون ریسپانس، بایت‌های همون فایل رو در جا
// عوض می‌کنه؛ بدونِ immutable مرورگر با ETag/Last-Modified همچنان رواعتبارسنجی می‌کنه
const STATIC_MEDIA_OPTIONS = { maxAge: '365d' };
// مسیرهای مشخص قبل از /storage عمومی — پروفایل از هر دو محل (قدیم و جدید) سرو می‌شود
app.use('/storage/profiles', express.static(path.join(__dirname, '../storage/profiles')));
app.use('/storage/profiles', express.static(path.join(__dirname, 'storage/profiles')));
app.use('/storage/carts', express.static(path.join(__dirname, '../storage/carts')));
app.use('/storage/carts', express.static(path.join(__dirname, 'storage/carts')));
app.use('/storage/posts', express.static(path.join(__dirname, 'storage/posts')));
app.use('/storage/posts', express.static(path.join(__dirname, '../storage/posts')));
app.use('/storage/messages', express.static(path.join(__dirname, '../storage/messages')));
app.use('/storage/messages', express.static(path.join(__dirname, 'storage/messages')));
app.use('/storage/shop-chat', express.static(path.join(__dirname, '../storage/shop-chat')));
app.use('/storage/shop-chat', express.static(path.join(__dirname, 'storage/shop-chat')));
app.use('/storage/chat-rooms', express.static(path.join(__dirname, '../storage/chat-rooms')));
app.use('/storage/chat-rooms', express.static(path.join(__dirname, 'storage/chat-rooms')));
app.use('/storage/tickets', express.static(path.join(__dirname, '../storage/tickets')));
app.use('/storage/tickets', express.static(path.join(__dirname, 'storage/tickets')));
app.use('/storage/advertising', express.static(path.join(__dirname, '../storage/advertising')));
app.use('/storage/advertising', express.static(path.join(__dirname, 'storage/advertising')));
app.use('/storage/services', express.static(path.join(__dirname, '../storage/services')));
app.use('/storage/services', express.static(path.join(__dirname, 'storage/services')));
app.use('/storage', express.static(path.join(__dirname, 'storage')));
app.use('/storage/profiles', express.static(path.join(__dirname, '../storage/profiles'), STATIC_MEDIA_OPTIONS));
app.use('/storage/profiles', express.static(path.join(__dirname, 'storage/profiles'), STATIC_MEDIA_OPTIONS));
app.use('/storage/carts', express.static(path.join(__dirname, '../storage/carts'), STATIC_MEDIA_OPTIONS));
app.use('/storage/carts', express.static(path.join(__dirname, 'storage/carts'), STATIC_MEDIA_OPTIONS));
app.use('/storage/posts', express.static(path.join(__dirname, 'storage/posts'), STATIC_MEDIA_OPTIONS));
app.use('/storage/posts', express.static(path.join(__dirname, '../storage/posts'), STATIC_MEDIA_OPTIONS));
app.use('/storage/messages', express.static(path.join(__dirname, '../storage/messages'), STATIC_MEDIA_OPTIONS));
app.use('/storage/messages', express.static(path.join(__dirname, 'storage/messages'), STATIC_MEDIA_OPTIONS));
app.use('/storage/shop-chat', express.static(path.join(__dirname, '../storage/shop-chat'), STATIC_MEDIA_OPTIONS));
app.use('/storage/shop-chat', express.static(path.join(__dirname, 'storage/shop-chat'), STATIC_MEDIA_OPTIONS));
app.use('/storage/chat-rooms', express.static(path.join(__dirname, '../storage/chat-rooms'), STATIC_MEDIA_OPTIONS));
app.use('/storage/chat-rooms', express.static(path.join(__dirname, 'storage/chat-rooms'), STATIC_MEDIA_OPTIONS));
app.use('/storage/tickets', express.static(path.join(__dirname, '../storage/tickets'), STATIC_MEDIA_OPTIONS));
app.use('/storage/tickets', express.static(path.join(__dirname, 'storage/tickets'), STATIC_MEDIA_OPTIONS));
app.use('/storage/advertising', express.static(path.join(__dirname, '../storage/advertising'), STATIC_MEDIA_OPTIONS));
app.use('/storage/advertising', express.static(path.join(__dirname, 'storage/advertising'), STATIC_MEDIA_OPTIONS));
app.use('/storage/services', express.static(path.join(__dirname, '../storage/services'), STATIC_MEDIA_OPTIONS));
app.use('/storage/services', express.static(path.join(__dirname, 'storage/services'), STATIC_MEDIA_OPTIONS));
app.use('/storage', express.static(path.join(__dirname, 'storage'), STATIC_MEDIA_OPTIONS));
require('./boot');
@@ -394,6 +413,13 @@ app.post('/api/v1/chat/file', [blockCheck, chatPeerBlock], async (req, res) => {
const uniqueFileName = `${Date.now()}-${crypto.randomUUID()}${path.extname(file.name || '')}`
const filePath = path.join(uploadDir, uniqueFileName)
await fs.move(file.path, filePath)
if (resolvedFileType === 'image' || (file.type || '').startsWith('image/')) {
await optimizeImageFile(filePath)
} else if (resolvedFileType === 'video' || (file.type || '').startsWith('video/')) {
optimizeVideoFaststart(filePath).catch((err) => {
console.error('[chat/file] video optimize background error:', err.message)
})
}
fileUrl = `/messages/${uniqueFileName}`
}