diff --git a/server.js b/server.js index ffb77ea..2676bbe 100644 --- a/server.js +++ b/server.js @@ -4,8 +4,8 @@ * * Strategy: * - SSH (server-side find+cat) for all NAS access — metadata and thumbnails - * - Disk-based thumbnail cache at CACHE_DIR (survives container restarts) - * - In-memory library metadata cache (reset on restart, cheap to rebuild) + * - Disk-based cache at CACHE_DIR for both metadata and thumbnails (survives container restarts) + * - In-memory library metadata cache (hot path after disk cache is warm) */ const express = require('express'); @@ -95,13 +95,16 @@ function listLibraries() { .map(e => e.replace('.library', '')); } -// --- In-memory library metadata cache --- +// --- In-memory library metadata cache (populated from disk cache on hit) --- let libraryCache = {}; -// --- Thumbnail: disk cache path helper --- +// --- Disk cache path helpers --- function thumbCachePath(library, imageId) { return path.join(CACHE_DIR, `${library}__${imageId}.png`); } +function metaCachePath(libName) { + return path.join(CACHE_DIR, `meta__${libName}.json`); +} // --- Routes --- @@ -113,15 +116,56 @@ app.get('/api/libraries', (req, res) => { } }); -app.get('/api/library/:name', (req, res) => { +app.get('/api/library/:name', async (req, res) => { try { const name = req.params.name + '.library'; - console.log(`Loading library: ${name} …`); + + // 1. In-memory cache (fastest — already loaded this session) + if (libraryCache[name]) { + const data = libraryCache[name]; + return res.json({ folders: data.meta.folders, imageCount: data.images.length, images: data.images, cached: 'memory' }); + } + + // 2. Disk cache (fast — survives restarts) + const diskPath = metaCachePath(name); + try { + const raw = await fsp.readFile(diskPath, 'utf8'); + const data = JSON.parse(raw); + libraryCache[name] = data; // also warm the in-memory cache + console.log(`Library ${name} loaded from disk cache (${data.images.length} images)`); + return res.json({ folders: data.meta.folders, imageCount: data.images.length, images: data.images, cached: 'disk' }); + } catch { /* cache miss — fall through to SSH */ } + + // 3. SSH fetch from NAS (slow — first time only) + console.log(`Loading library: ${name} from NAS via SSH…`); const t0 = Date.now(); const data = loadLibrary(name); libraryCache[name] = data; - console.log(`Loaded ${data.images.length} images in ${Date.now() - t0}ms`); - res.json({ folders: data.meta.folders, imageCount: data.images.length, images: data.images }); + console.log(`Loaded ${data.images.length} images in ${Date.now() - t0}ms — writing to disk cache`); + + // Write to disk cache (fire-and-forget) + fsp.writeFile(diskPath, JSON.stringify(data)).catch(err => + console.warn(`Failed to write metadata cache for ${name}:`, err.message) + ); + + res.json({ folders: data.meta.folders, imageCount: data.images.length, images: data.images, cached: 'miss' }); + } catch (e) { + console.error(e); + res.status(500).json({ error: e.message }); + } +}); + +// Force a fresh SSH fetch and update the disk cache (call this when library contents change) +app.post('/api/library/:name/refresh', async (req, res) => { + try { + const name = req.params.name + '.library'; + console.log(`Refreshing library: ${name} …`); + const t0 = Date.now(); + const data = loadLibrary(name); + libraryCache[name] = data; + await fsp.writeFile(metaCachePath(name), JSON.stringify(data)); + console.log(`Refreshed ${data.images.length} images in ${Date.now() - t0}ms`); + res.json({ ok: true, imageCount: data.images.length, ms: Date.now() - t0 }); } catch (e) { console.error(e); res.status(500).json({ error: e.message });