webupdatev1
This commit is contained in:
129
backend/middleware/imageOptimization.js
Normal file
129
backend/middleware/imageOptimization.js
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Image Optimization Middleware
|
||||
* High-performance image serving with streaming and caching
|
||||
*/
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const fsPromises = require("fs").promises;
|
||||
const logger = require("../config/logger");
|
||||
|
||||
// Cache for image metadata (not content)
|
||||
const metadataCache = new Map();
|
||||
const METADATA_CACHE_TTL = 600000; // 10 minutes
|
||||
const METADATA_CACHE_MAX = 1000;
|
||||
|
||||
// Image mime types
|
||||
const MIME_TYPES = {
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".svg": "image/svg+xml",
|
||||
".ico": "image/x-icon",
|
||||
".avif": "image/avif",
|
||||
};
|
||||
|
||||
/**
|
||||
* Get or cache image metadata
|
||||
*/
|
||||
async function getImageMetadata(filePath) {
|
||||
const cached = metadataCache.get(filePath);
|
||||
if (cached && Date.now() - cached.timestamp < METADATA_CACHE_TTL) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await fsPromises.stat(filePath);
|
||||
const metadata = {
|
||||
exists: true,
|
||||
size: stats.size,
|
||||
mtime: stats.mtime.getTime(),
|
||||
etag: `"${stats.size}-${stats.mtime.getTime()}"`,
|
||||
lastModified: stats.mtime.toUTCString(),
|
||||
};
|
||||
|
||||
// LRU eviction
|
||||
if (metadataCache.size >= METADATA_CACHE_MAX) {
|
||||
const firstKey = metadataCache.keys().next().value;
|
||||
metadataCache.delete(firstKey);
|
||||
}
|
||||
|
||||
metadataCache.set(filePath, { data: metadata, timestamp: Date.now() });
|
||||
return metadata;
|
||||
} catch {
|
||||
const notFound = { exists: false };
|
||||
metadataCache.set(filePath, { data: notFound, timestamp: Date.now() });
|
||||
return notFound;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve optimized images with streaming and aggressive caching
|
||||
*/
|
||||
const imageOptimization = (uploadsDir) => {
|
||||
return async (req, res, next) => {
|
||||
// Only handle image requests
|
||||
const ext = path.extname(req.path).toLowerCase();
|
||||
if (!MIME_TYPES[ext]) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const imagePath = path.join(uploadsDir, req.path.replace("/uploads/", ""));
|
||||
|
||||
// Get cached metadata
|
||||
const metadata = await getImageMetadata(imagePath);
|
||||
if (!metadata.exists) {
|
||||
return next();
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if client has cached version (304 Not Modified)
|
||||
const ifNoneMatch = req.get("if-none-match");
|
||||
const ifModifiedSince = req.get("if-modified-since");
|
||||
|
||||
if (
|
||||
ifNoneMatch === metadata.etag ||
|
||||
ifModifiedSince === metadata.lastModified
|
||||
) {
|
||||
return res.status(304).end();
|
||||
}
|
||||
|
||||
// Set aggressive caching headers
|
||||
res.set({
|
||||
"Content-Type": MIME_TYPES[ext],
|
||||
"Content-Length": metadata.size,
|
||||
"Cache-Control": "public, max-age=31536000, immutable", // 1 year
|
||||
ETag: metadata.etag,
|
||||
"Last-Modified": metadata.lastModified,
|
||||
Vary: "Accept-Encoding",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
});
|
||||
|
||||
// Use streaming for efficient memory usage
|
||||
const readStream = fs.createReadStream(imagePath, {
|
||||
highWaterMark: 64 * 1024, // 64KB chunks
|
||||
});
|
||||
|
||||
readStream.on("error", (error) => {
|
||||
logger.error("Image stream error:", {
|
||||
path: imagePath,
|
||||
error: error.message,
|
||||
});
|
||||
if (!res.headersSent) {
|
||||
res.status(500).end();
|
||||
}
|
||||
});
|
||||
|
||||
readStream.pipe(res);
|
||||
} catch (error) {
|
||||
logger.error("Image serve error:", {
|
||||
path: imagePath,
|
||||
error: error.message,
|
||||
});
|
||||
next();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = { imageOptimization };
|
||||
Reference in New Issue
Block a user