const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const axios = require('axios'); const IMAGES_DIR = path.join(__dirname, '..', 'data', 'images'); // Ensure images directory exists if (!fs.existsSync(IMAGES_DIR)) { fs.mkdirSync(IMAGES_DIR, { recursive: true }); } /** * Calculate SHA256 hash of a file */ function calculateFileHash(filePath) { return new Promise((resolve, reject) => { const hash = crypto.createHash('sha256'); const stream = fs.createReadStream(filePath); stream.on('error', reject); stream.on('data', chunk => hash.update(chunk)); stream.on('end', () => resolve(hash.digest('hex'))); }); } /** * Generate a unique filename */ function generateFilename(sourceUrl, timestamp = Date.now()) { const hash = crypto.createHash('md5').update(sourceUrl + timestamp).digest('hex'); const ext = getFileExtension(sourceUrl); return `image_${timestamp}_${hash}${ext}`; } /** * Get file extension from URL */ function getFileExtension(url) { try { const urlObj = new URL(url); const pathname = urlObj.pathname; const ext = path.extname(pathname); // If no extension, try to get from content-type or use default if (!ext || ext.length > 5) { return '.jpg'; } return ext; } catch (err) { return '.jpg'; } } /** * Download an image and save it to storage */ async function downloadAndSaveImage(sourceUrl, options = {}) { const { timeout = 30000, maxSize = 50 * 1024 * 1024 } = options; try { // Download image const response = await axios({ url: sourceUrl, method: 'GET', responseType: 'arraybuffer', timeout, maxContentLength: maxSize, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } }); const filename = generateFilename(sourceUrl); const filePath = path.join(IMAGES_DIR, filename); const fileBuffer = Buffer.from(response.data); const filesize = fileBuffer.length; const file_hash = crypto.createHash('sha256').update(fileBuffer).digest('hex'); const mime_type = response.headers['content-type'] || 'image/jpeg'; // Save file fs.writeFileSync(filePath, fileBuffer); return { filename, file_path: filePath, relative_path: path.relative(path.join(__dirname, '..'), filePath), filesize, file_hash, mime_type, source_url: sourceUrl, success: true }; } catch (err) { console.error(`Failed to download image from ${sourceUrl}:`, err.message); return { source_url: sourceUrl, success: false, error: err.message }; } } /** * Verify image integrity using stored hash */ async function verifyImageIntegrity(filePath, expectedHash) { try { if (!fs.existsSync(filePath)) { return { valid: false, reason: 'File does not exist' }; } const actualHash = await calculateFileHash(filePath); if (actualHash !== expectedHash) { return { valid: false, reason: 'Hash mismatch', expectedHash, actualHash }; } return { valid: true }; } catch (err) { return { valid: false, reason: err.message }; } } /** * Delete image file */ function deleteImageFile(filePath) { return new Promise((resolve, reject) => { try { if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); resolve(true); } else { resolve(false); } } catch (err) { reject(err); } }); } /** * Get image file as buffer */ function getImageBuffer(filePath) { return new Promise((resolve, reject) => { fs.readFile(filePath, (err, data) => { if (err) reject(err); else resolve(data); }); }); } /** * Get storage statistics */ function getStorageStats() { return new Promise((resolve, reject) => { try { let totalSize = 0; let fileCount = 0; if (!fs.existsSync(IMAGES_DIR)) { resolve({ totalSize: 0, fileCount: 0, dataDir: IMAGES_DIR }); return; } const files = fs.readdirSync(IMAGES_DIR); files.forEach((file) => { const filePath = path.join(IMAGES_DIR, file); const stats = fs.statSync(filePath); if (stats.isFile()) { totalSize += stats.size; fileCount++; } }); resolve({ totalSize, totalSizeGB: (totalSize / (1024 ** 3)).toFixed(2), fileCount, dataDir: IMAGES_DIR }); } catch (err) { reject(err); } }); } /** * Clean up orphaned files (files in storage but not in database) */ async function cleanupOrphanedFiles(db) { try { if (!fs.existsSync(IMAGES_DIR)) { return { cleaned: 0 }; } const files = fs.readdirSync(IMAGES_DIR); let cleaned = 0; for (const file of files) { const filePath = path.join(IMAGES_DIR, file); // Check if file exists in database const existsInDb = await new Promise((resolve) => { db.get( 'SELECT id FROM images WHERE filename = ?', [file], (err, row) => resolve(!err && !!row) ); }); if (!existsInDb) { try { fs.unlinkSync(filePath); cleaned++; } catch (err) { console.error(`Failed to delete orphaned file ${file}:`, err.message); } } } return { cleaned }; } catch (err) { console.error('Cleanup error:', err); return { cleaned: 0, error: err.message }; } } module.exports = { downloadAndSaveImage, verifyImageIntegrity, deleteImageFile, getImageBuffer, getStorageStats, calculateFileHash, generateFilename, getFileExtension, cleanupOrphanedFiles, IMAGES_DIR };