130 lines
3.4 KiB
JavaScript
130 lines
3.4 KiB
JavaScript
|
|
/**
|
||
|
|
* 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 };
|