Files
SkyArtShop/backend/middleware/imageOptimization.js

130 lines
3.4 KiB
JavaScript
Raw Normal View History

2026-01-04 17:52:37 -06:00
/**
* 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 };