/** * File hashing utilities for cache key generation */ export class FileHasher { private static readonly CHUNK_SIZE = 64 * 1024; // 64KB chunks for hashing /** * Generate a content-based hash for a file * Uses first + last + middle chunks to create a reasonably unique hash * without reading the entire file (which would be expensive for large files) */ static async generateContentHash(file: File): Promise { const chunks = await this.getFileChunks(file); const combined = await this.combineChunks(chunks); return await this.hashArrayBuffer(combined); } /** * Generate a fast hash based on file metadata * Faster but less collision-resistant than content hash */ static generateMetadataHash(file: File): string { const data = `${file.name}-${file.size}-${file.lastModified}-${file.type}`; return this.simpleHash(data); } /** * Generate a hybrid hash that balances speed and uniqueness * Uses metadata + small content sample */ static async generateHybridHash(file: File): Promise { const metadataHash = this.generateMetadataHash(file); // For small files, use full content hash if (file.size <= 1024 * 1024) { // 1MB const contentHash = await this.generateContentHash(file); return `${metadataHash}-${contentHash}`; } // For large files, use first chunk only const firstChunk = file.slice(0, this.CHUNK_SIZE); const firstChunkBuffer = await firstChunk.arrayBuffer(); const firstChunkHash = await this.hashArrayBuffer(firstChunkBuffer); return `${metadataHash}-${firstChunkHash}`; } private static async getFileChunks(file: File): Promise { const chunks: ArrayBuffer[] = []; // First chunk if (file.size > 0) { const firstChunk = file.slice(0, Math.min(this.CHUNK_SIZE, file.size)); chunks.push(await firstChunk.arrayBuffer()); } // Middle chunk (if file is large enough) if (file.size > this.CHUNK_SIZE * 2) { const middleStart = Math.floor(file.size / 2) - Math.floor(this.CHUNK_SIZE / 2); const middleEnd = middleStart + this.CHUNK_SIZE; const middleChunk = file.slice(middleStart, middleEnd); chunks.push(await middleChunk.arrayBuffer()); } // Last chunk (if file is large enough and different from first) if (file.size > this.CHUNK_SIZE) { const lastStart = Math.max(file.size - this.CHUNK_SIZE, this.CHUNK_SIZE); const lastChunk = file.slice(lastStart); chunks.push(await lastChunk.arrayBuffer()); } return chunks; } private static async combineChunks(chunks: ArrayBuffer[]): Promise { const totalLength = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0); const combined = new Uint8Array(totalLength); let offset = 0; for (const chunk of chunks) { combined.set(new Uint8Array(chunk), offset); offset += chunk.byteLength; } return combined.buffer; } private static async hashArrayBuffer(buffer: ArrayBuffer): Promise { // Use Web Crypto API for proper hashing if (crypto.subtle) { const hashBuffer = await crypto.subtle.digest('SHA-256', buffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } // Fallback for environments without crypto.subtle return this.simpleHash(Array.from(new Uint8Array(buffer)).join('')); } private static simpleHash(str: string): string { let hash = 0; if (str.length === 0) return hash.toString(); for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash).toString(16); } /** * Validate that a file matches its expected hash * Useful for detecting file corruption or changes */ static async validateFileHash(file: File, expectedHash: string): Promise { try { const actualHash = await this.generateHybridHash(file); return actualHash === expectedHash; } catch (error) { console.error('Hash validation failed:', error); return false; } } }