// services/imageProxy.js import crypto from 'crypto'; import { createLogger } from '../utils/logger.js'; const logger = createLogger('IMAGE-PROXY'); class ImageProxyService { constructor() { this.urlMap = new Map(); this.reverseMap = new Map(); this.SECRET_KEY = process.env.IMAGE_PROXY_SECRET || 'default-secret-change-me'; this.stats = { created: 0, served: 0, errors: 0 }; this._loadMappings(); } /** * Load mappings from persistent storage */ async _loadMappings() { try { // Since loadImageMappings was removed, just initialize empty maps logger.info('MAPPINGS-LOADED', 'Starting with empty mappings (loadImageMappings removed)'); } catch (error) { logger.error('MAPPINGS-LOAD-FAILED', 'Failed to initialize mappings', error); } } /** * Save mappings to persistent storage (disabled) */ async _saveMappings() { try { // Since saveImageMappings was removed, just log logger.info('MAPPINGS-SAVED', `Would save ${this.urlMap.size} mappings (disabled)`); } catch (error) { logger.error('MAPPINGS-SAVE-FAILED', 'Failed to save image mappings', error); } } /** * Generate a unique, secure ID for an image URL */ generateImageId(url) { if (!url || url === 'N/A' || url === 'Unknown' || url === '') { return null; } // Check if we already have an ID for this URL if (this.reverseMap.has(url)) { const existingId = this.reverseMap.get(url); logger.info('ID-EXISTS', `Reusing existing ID for URL`, { id: existingId, url: url.substring(0, 50) + '...' }); return existingId; } // Create a hash-based ID const hash = crypto .createHmac('sha256', this.SECRET_KEY) .update(url) .digest('hex') .substring(0, 16); const imageId = `img_${hash}`; // Store bidirectional mapping this.urlMap.set(imageId, url); this.reverseMap.set(url, imageId); this.stats.created++; // Save mappings to persistent storage (disabled) this._saveMappings(); logger.success('ID-CREATED', `Created image ID`, { id: imageId, url: url.substring(0, 50) + '...', totalMappings: this.urlMap.size }); return imageId; } /** * Get actual URL from image ID */ getActualUrl(imageId) { const url = this.urlMap.get(imageId); if (!url) { logger.warn('URL-NOT-FOUND', `No mapping found for image ID: ${imageId}`, { totalMappings: this.urlMap.size, availableIds: Array.from(this.urlMap.keys()).slice(0, 5) }); this.stats.errors++; return null; } logger.info('URL-FOUND', `Retrieved URL for ${imageId}`); this.stats.served++; return url; } /** * Create proxy URL */ createProxyUrl(originalUrl, baseUrl = '') { if (!originalUrl || originalUrl === 'N/A' || originalUrl === 'Unknown' || originalUrl === '') { logger.warn('INVALID-URL', 'Cannot create proxy for invalid URL', { originalUrl }); return null; } // Don't proxy if it's already a proxy URL if (originalUrl.includes('/api/image/img_')) { logger.info('ALREADY-PROXIED', 'URL is already proxied', { originalUrl }); return originalUrl; } const imageId = this.generateImageId(originalUrl); if (!imageId) { logger.error('ID-GENERATION-FAILED', 'Failed to generate image ID'); return originalUrl; // Return original URL as fallback } // Determine base URL based on environment let railwayUrl; // Robust environment check const env = (process.env.NODE_ENV || '').trim().toLowerCase(); const isDev = env === 'development' || env === 'dev'; if (isDev) { // In development, prefer the local base URL railwayUrl = baseUrl || 'http://localhost:3000'; logger.info('PROXY-ENV', 'Using local base URL for development', { env: process.env.NODE_ENV, parsedEnv: env, railwayUrl }); } else { // In production, prefer the Railway URL railwayUrl = process.env.RAILWAY_STATIC_URL || baseUrl; logger.info('PROXY-ENV', 'Using production URL', { env: process.env.NODE_ENV, parsedEnv: env, railwayUrl }); } // Remove trailing slash if present to prevent double slashes if (railwayUrl.endsWith('/')) { railwayUrl = railwayUrl.slice(0, -1); } const proxyUrl = `${railwayUrl}/api/image/${imageId}`; logger.success('PROXY-CREATED', `Proxy URL created`, { imageId, proxyUrl, originalUrl: originalUrl.substring(0, 50) + '...' }); return proxyUrl; } /** * Get stats */ getStats() { return { totalMappings: this.urlMap.size, cacheSize: this.reverseMap.size, stats: this.stats, sampleIds: Array.from(this.urlMap.keys()).slice(0, 10) }; } /** * Clear old mappings (optional cleanup) */ cleanup() { const before = this.urlMap.size; // Keep only last 10000 mappings if (this.urlMap.size > 10000) { const entries = Array.from(this.urlMap.entries()); const toKeep = entries.slice(-10000); this.urlMap.clear(); this.reverseMap.clear(); toKeep.forEach(([id, url]) => { this.urlMap.set(id, url); this.reverseMap.set(url, id); }); // Save updated mappings (disabled) this._saveMappings(); logger.info('CLEANUP', `Cleaned up ${before - this.urlMap.size} old mappings`, { before, after: this.urlMap.size }); } else { logger.info('CLEANUP', 'No cleanup needed', { currentSize: this.urlMap.size, maxSize: 10000 }); } } /** * Debug: List all mappings */ listAllMappings() { const mappings = []; this.urlMap.forEach((url, id) => { mappings.push({ id, url: url.substring(0, 100) }); }); return mappings; } } // Export singleton instance const imageProxy = new ImageProxyService(); export default imageProxy;