// src/services/imageHashService.ts import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; import { promises as fsPromises } from 'fs'; // Define the path to the JSON file that will store our hashes // In a real production app, this would be replaced with a database // For Vercel, we'll use the tmp directory which is writable const HASH_FILE_PATH = process.env.NODE_ENV === 'production' ? path.join('/tmp', 'image-hashes.json') // Use /tmp for Vercel : path.join(process.cwd(), 'image-hashes.json'); // Interface for storing image hash data interface ImageHashData { [hash: string]: { timestamp: string; description?: string; }; } /** * Initialize the hash file if it doesn't exist */ async function initializeHashFile(): Promise { try { try { await fsPromises.access(HASH_FILE_PATH); console.log('Hash file exists at:', HASH_FILE_PATH); } catch (error) { // File doesn't exist, create an empty one console.log('Creating hash file at:', HASH_FILE_PATH); // Create directory if it doesn't exist (for /tmp path) const dir = path.dirname(HASH_FILE_PATH); try { await fsPromises.mkdir(dir, { recursive: true }); } catch (mkdirError) { // Ignore directory exists error console.log('Note: Directory already exists or cannot be created'); } await fsPromises.writeFile(HASH_FILE_PATH, JSON.stringify({})); } } catch (error) { console.error('Error initializing hash file:', error); // Don't throw - in Vercel we'll continue without hash checking } } /** * Read all stored image hashes */ export async function readImageHashes(): Promise { await initializeHashFile(); try { const data = await fsPromises.readFile(HASH_FILE_PATH, 'utf8'); return JSON.parse(data) as ImageHashData; } catch (error) { console.error('Error reading image hashes file:', error); return {}; } } /** * Generate a SHA-256 hash for an image buffer */ export function generateImageHash(imageBuffer: Buffer): string { if (!imageBuffer || !(imageBuffer instanceof Buffer)) { throw new Error('Invalid image buffer provided'); } if (imageBuffer.length === 0) { throw new Error('Empty image buffer'); } return crypto.createHash('sha256').update(imageBuffer).digest('hex'); } /** * Check if an image hash already exists */ export async function isImageHashDuplicate(hash: string): Promise { const hashes = await readImageHashes(); return !!hashes[hash]; } /** * Store a new image hash */ export async function storeImageHash(hash: string, description?: string): Promise { const hashes = await readImageHashes(); hashes[hash] = { timestamp: new Date().toISOString(), description }; try { await fsPromises.writeFile(HASH_FILE_PATH, JSON.stringify(hashes, null, 2)); } catch (error) { console.error('Error writing image hash:', error); throw new Error('Failed to store image hash'); } } /** * Check if an image is a duplicate and store if not * Returns true if the image is a duplicate, false otherwise */ export async function checkAndStoreImageHash( imageBuffer: Buffer, description?: string ): Promise { // In Vercel serverless functions, file operations may fail due to permissions // We'll still calculate the hash but handle failures gracefully try { // Validate buffer if (!imageBuffer || !(imageBuffer instanceof Buffer)) { console.error('Invalid image buffer provided to checkAndStoreImageHash'); throw new Error('Invalid image buffer provided'); } if (imageBuffer.length === 0) { console.error('Empty image buffer provided to checkAndStoreImageHash'); throw new Error('Empty image buffer'); } const hash = generateImageHash(imageBuffer); console.log(`Generated image hash: ${hash.substring(0, 8)}...`); // Read the current hashes - log but don't throw on error try { const hashes = await readImageHashes(); const isDuplicate = !!hashes[hash]; // Log before storing if (isDuplicate) { console.log(`Duplicate image detected with hash: ${hash.substring(0, 8)}...`); } else { console.log(`New image with hash: ${hash.substring(0, 8)}...`); } // If not a duplicate or if we're updating with a description, update the hash file if (!isDuplicate || description) { hashes[hash] = { timestamp: new Date().toISOString(), description: description || hashes[hash]?.description }; try { await fsPromises.writeFile(HASH_FILE_PATH, JSON.stringify(hashes, null, 2)); console.log(`Successfully stored hash ${hash.substring(0, 8)}...`); } catch (writeError) { console.error('Warning: Could not write to hash file:', writeError.message); // Continue without throwing - the hash check still worked } } return isDuplicate; } catch (readError) { // If we can't read the hash file, assume it's not a duplicate console.error('Warning: Could not read hash file:', readError.message); // Try to create a new hash file with just this hash try { const singleHash = { [hash]: { timestamp: new Date().toISOString(), description } }; await fsPromises.writeFile(HASH_FILE_PATH, JSON.stringify(singleHash, null, 2)); } catch (createError) { console.error('Warning: Could not create new hash file:', createError.message); } return false; // Not a duplicate since we couldn't check } } catch (error) { console.error('Error in checkAndStoreImageHash:', error); // Return false instead of throwing to avoid breaking the app flow // This means we'll treat errors as "not a duplicate" and continue processing return false; } } /** * Get details about a stored hash */ export async function getHashDetails(hash: string): Promise { const hashes = await readImageHashes(); return hashes[hash] || null; }