import express from 'express'; import path from 'path'; import { fileURLToPath } from 'url'; import fs from 'fs/promises'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export function startServer(categorySyncer) { const app = express(); const PORT = process.env.SERVER_PORT || 3000; const HOST = process.env.SERVER_HOST || '0.0.0.0'; const CACHE_DIR = process.env.CACHE_LOCATION || './cache'; // Cache for ETags and data const cache = { categories: { etag: null, data: null }, html: { etag: null, data: null } }; // Function to calculate ETag for categories async function updateCategoriesCache() { try { const treePath = path.join(CACHE_DIR, 'category_tree.json'); const data = await fs.readFile(treePath, 'utf-8'); const crypto = await import('crypto'); cache.categories.etag = crypto.createHash('md5').update(data).digest('hex'); cache.categories.data = data; } catch (err) { // Silently skip if file doesn't exist yet (will be created on first sync) if (err.code !== 'ENOENT') { console.error('Error updating categories cache:', err); } } } // Function to calculate ETag for HTML async function updateHtmlCache() { try { const htmlPath = path.join(__dirname, 'index.html'); const data = await fs.readFile(htmlPath, 'utf-8'); const crypto = await import('crypto'); cache.html.etag = crypto.createHash('md5').update(data).digest('hex'); cache.html.data = data; } catch (err) { console.error('Error updating HTML cache:', err); } } // Initialize caches on startup updateHtmlCache(); updateCategoriesCache(); // Update categories cache when sync completes if (categorySyncer) { categorySyncer.on('synced', ({ changed }) => { if (changed) { updateCategoriesCache(); } }); } // Serve category tree JSON (with ETag for conditional caching) app.get('/api/categories', async (req, res) => { try { // Check if client has cached version if (req.headers['if-none-match'] === cache.categories.etag) { return res.status(304).end(); // Not Modified } // Set cache headers with ETag res.set('Cache-Control', 'public, max-age=60, must-revalidate'); res.set('ETag', cache.categories.etag); res.json(JSON.parse(cache.categories.data)); } catch (err) { res.status(500).json({ error: 'Failed to load category tree' }); } }); // Serve category images (long cache - images rarely change) app.get('/img/cat/:id.avif', (req, res) => { const { id } = req.params; const imagePath = path.join(CACHE_DIR, 'img', 'categories', `${id}.avif`); // Cache images for 1 year (immutable content) res.set('Cache-Control', 'public, max-age=31536000, immutable'); res.sendFile(path.resolve(imagePath), (err) => { if (err) { res.status(404).send('Image not found'); } }); }); // Serve index.html (with ETag for conditional caching) app.get('/', async (req, res) => { try { // Check if client has cached version if (req.headers['if-none-match'] === cache.html.etag) { return res.status(304).end(); // Not Modified } // Set cache headers with ETag res.set('Cache-Control', 'public, max-age=300, must-revalidate'); res.set('ETag', cache.html.etag); res.set('Content-Type', 'text/html'); res.send(cache.html.data); } catch (err) { console.error('Error serving index.html:', err); res.status(500).send('Error loading page'); } }); app.listen(PORT, HOST, () => { console.log(`🌐 Server running on http://${HOST}:${PORT}`); }); }