180 lines
5.6 KiB
JavaScript
180 lines
5.6 KiB
JavaScript
const crypto = require('crypto');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
/**
|
|
* Cache busting utility for preventing browser caching of static assets
|
|
*/
|
|
class CacheBusting {
|
|
constructor() {
|
|
// Generate a unique version identifier on server start
|
|
this.version = this.generateVersion();
|
|
this.fileHashes = new Map();
|
|
}
|
|
|
|
/**
|
|
* Generate a version identifier based on server start time and random data
|
|
* @returns {string} Version string
|
|
*/
|
|
generateVersion() {
|
|
const timestamp = Date.now();
|
|
const random = crypto.randomBytes(4).toString('hex');
|
|
return `${timestamp}-${random}`;
|
|
}
|
|
|
|
/**
|
|
* Get cache busting version for the application
|
|
* @returns {string} Version string
|
|
*/
|
|
getVersion() {
|
|
return this.version;
|
|
}
|
|
|
|
/**
|
|
* Generate a file hash for specific file-based cache busting
|
|
* @param {string} filePath - Path to the file
|
|
* @returns {string} File hash or version fallback
|
|
*/
|
|
getFileHash(filePath) {
|
|
try {
|
|
if (this.fileHashes.has(filePath)) {
|
|
return this.fileHashes.get(filePath);
|
|
}
|
|
|
|
const fullPath = path.join(__dirname, '..', 'public', filePath);
|
|
if (fs.existsSync(fullPath)) {
|
|
const content = fs.readFileSync(fullPath);
|
|
const hash = crypto.createHash('md5').update(content).digest('hex').substring(0, 8);
|
|
this.fileHashes.set(filePath, hash);
|
|
return hash;
|
|
}
|
|
} catch (error) {
|
|
console.warn(`Cache busting: Could not hash file ${filePath}:`, error.message);
|
|
}
|
|
|
|
// Fallback to version
|
|
return this.version;
|
|
}
|
|
|
|
/**
|
|
* Add cache busting parameter to a URL
|
|
* @param {string} url - Original URL
|
|
* @param {boolean} useFileHash - Whether to use file-specific hash
|
|
* @returns {string} URL with cache busting parameter
|
|
*/
|
|
bustCache(url, useFileHash = false) {
|
|
if (!url) return url;
|
|
|
|
const separator = url.includes('?') ? '&' : '?';
|
|
const version = useFileHash ? this.getFileHash(url) : this.version;
|
|
|
|
return `${url}${separator}v=${version}`;
|
|
}
|
|
|
|
/**
|
|
* Get cache headers for static assets
|
|
* @param {boolean} longTerm - Whether to use long-term caching
|
|
* @returns {object} Cache headers
|
|
*/
|
|
getCacheHeaders(longTerm = false) {
|
|
if (longTerm) {
|
|
// Long-term caching for versioned assets
|
|
return {
|
|
'Cache-Control': 'public, max-age=31536000, immutable', // 1 year
|
|
'ETag': this.version
|
|
};
|
|
} else {
|
|
// Short-term caching for frequently changing content
|
|
return {
|
|
'Cache-Control': 'public, max-age=300', // 5 minutes
|
|
'ETag': this.version
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get no-cache headers for dynamic content
|
|
* @returns {object} No-cache headers
|
|
*/
|
|
getNoCacheHeaders() {
|
|
return {
|
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
'Pragma': 'no-cache',
|
|
'Expires': '0'
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Middleware to add cache busting to HTML responses
|
|
* @returns {function} Express middleware
|
|
*/
|
|
htmlMiddleware() {
|
|
return (req, res, next) => {
|
|
// Store original send method
|
|
const originalSend = res.send;
|
|
|
|
// Override send method to modify HTML content
|
|
res.send = (body) => {
|
|
if (typeof body === 'string' && res.get('Content-Type')?.includes('text/html')) {
|
|
// Add cache busting to CSS and JS files
|
|
body = body.replace(
|
|
/(href|src)=["']([^"']+\.(css|js))["']/g,
|
|
(match, attr, url, ext) => {
|
|
// Skip external URLs
|
|
if (url.startsWith('http') || url.startsWith('//')) {
|
|
return match;
|
|
}
|
|
|
|
const bustedUrl = this.bustCache(url, true);
|
|
return `${attr}="${bustedUrl}"`;
|
|
}
|
|
);
|
|
|
|
// Add version info to HTML for debugging
|
|
body = body.replace(
|
|
'</head>',
|
|
` <meta name="app-version" content="${this.version}">\n</head>`
|
|
);
|
|
}
|
|
|
|
// Call original send method
|
|
originalSend.call(res, body);
|
|
};
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Middleware to set cache headers for static files
|
|
* @returns {function} Express middleware
|
|
*/
|
|
staticCacheMiddleware() {
|
|
return (req, res, next) => {
|
|
const ext = path.extname(req.path).toLowerCase();
|
|
const isVersioned = req.query.v !== undefined;
|
|
|
|
// Set cache headers based on file type and versioning
|
|
if (['.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg'].includes(ext)) {
|
|
const headers = isVersioned ?
|
|
this.getCacheHeaders(true) :
|
|
this.getCacheHeaders(false);
|
|
|
|
Object.keys(headers).forEach(key => {
|
|
res.set(key, headers[key]);
|
|
});
|
|
}
|
|
|
|
next();
|
|
};
|
|
}
|
|
}
|
|
|
|
// Create singleton instance
|
|
const cacheBusting = new CacheBusting();
|
|
|
|
module.exports = {
|
|
CacheBusting,
|
|
cacheBusting
|
|
};
|