Add content-based hashing to recipe images for proper cache invalidation while maintaining graceful degradation through dual file storage. Changes: - Add imageHash utility with SHA-256 content hashing (8-char) - Update Recipe model to store hashed filenames in images[0].mediapath - Modify image upload endpoint to save both hashed and unhashed versions - Update frontend components to use images[0].mediapath with fallback - Add migration endpoint to hash existing images (production-only) - Update image delete/rename endpoints to handle both file versions Images are now stored as: - recipe.a1b2c3d4.webp (hashed, cached forever) - recipe.webp (unhashed, graceful degradation fallback) Database stores hashed filename for cache busting, while unhashed version remains on disk for backward compatibility and manual uploads.
51 lines
1.6 KiB
TypeScript
51 lines
1.6 KiB
TypeScript
import crypto from 'crypto';
|
|
import fs from 'fs';
|
|
|
|
/**
|
|
* Generates an 8-character hash from image file content
|
|
* Uses SHA-256 for reliable, content-based hashing
|
|
* @param filePath - Path to the image file
|
|
* @returns 8-character hex hash
|
|
*/
|
|
export function generateImageHash(filePath: string): string {
|
|
const fileBuffer = fs.readFileSync(filePath);
|
|
const hashSum = crypto.createHash('sha256');
|
|
hashSum.update(fileBuffer);
|
|
const hash = hashSum.digest('hex');
|
|
return hash.substring(0, 8);
|
|
}
|
|
|
|
/**
|
|
* Generates an 8-character hash from Buffer content
|
|
* @param buffer - Image file buffer
|
|
* @returns 8-character hex hash
|
|
*/
|
|
export function generateImageHashFromBuffer(buffer: Buffer): string {
|
|
const hashSum = crypto.createHash('sha256');
|
|
hashSum.update(buffer);
|
|
const hash = hashSum.digest('hex');
|
|
return hash.substring(0, 8);
|
|
}
|
|
|
|
/**
|
|
* Creates a filename with hash for cache busting
|
|
* @param basename - Base name without extension (e.g., "maccaroni")
|
|
* @param hash - 8-character hash
|
|
* @returns Filename with hash (e.g., "maccaroni.a1b2c3d4.webp")
|
|
*/
|
|
export function getHashedFilename(basename: string, hash: string): string {
|
|
return `${basename}.${hash}.webp`;
|
|
}
|
|
|
|
/**
|
|
* Extracts basename from a potentially hashed filename
|
|
* @param filename - Filename (e.g., "maccaroni.a1b2c3d4.webp" or "maccaroni.webp")
|
|
* @returns Basename without hash or extension (e.g., "maccaroni")
|
|
*/
|
|
export function extractBasename(filename: string): string {
|
|
// Remove .webp extension
|
|
const withoutExt = filename.replace(/\.webp$/, '');
|
|
// Remove hash if present (8 hex chars preceded by a dot)
|
|
return withoutExt.replace(/\.[a-f0-9]{8}$/, '');
|
|
}
|