ranger-app/src/services/imageHashService.ts
2025-03-11 18:00:25 -04:00

196 lines
6.1 KiB
TypeScript

// 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<void> {
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<ImageHashData> {
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<boolean> {
const hashes = await readImageHashes();
return !!hashes[hash];
}
/**
* Store a new image hash
*/
export async function storeImageHash(hash: string, description?: string): Promise<void> {
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<boolean> {
// 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<any | null> {
const hashes = await readImageHashes();
return hashes[hash] || null;
}