import express from 'express'; import swaggerUi from 'swagger-ui-express'; import YAML from 'yamljs'; import path from 'path'; import { fileURLToPath } from 'url'; import fs from 'fs/promises'; import crypto from 'crypto'; import { getAuthorData, getAuthorFilters } from 'stihirus-reader'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); const port = process.env.PORT || 7860; const CACHE_DIR = __dirname; // Use current directory for cache files const CACHE_DURATION_MS = 60 * 60 * 1000; // 1 hour const swaggerDocument = YAML.load(path.join(__dirname, 'openapi.yaml')); app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); // No need for ensureCacheDir anymore function generateCacheKey(prefix, identifier, queryParams = {}) { const identifierPart = String(identifier).replace(/[^a-zA-Z0-9_-]/g, '_'); let queryPart = ''; const sortedKeys = Object.keys(queryParams).sort(); sortedKeys.forEach(key => { if (queryParams[key] !== undefined && queryParams[key] !== null) { queryPart += `_${key}_${String(queryParams[key])}`; } else if (queryParams[key] === null) { queryPart += `_${key}_null`; } }); // Add prefix to avoid potential collisions with other files return `_cache_${prefix}_${identifierPart}${queryPart}.json`; } async function readCache(key) { const filePath = path.join(CACHE_DIR, key); try { const stats = await fs.stat(filePath); const now = Date.now(); const mtime = stats.mtime.getTime(); const isStale = (now - mtime) > CACHE_DURATION_MS; const content = await fs.readFile(filePath, 'utf-8'); const data = JSON.parse(content); return { data, isStale, exists: true, mtime }; } catch (err) { if (err.code === 'ENOENT') { return { exists: false }; } console.error(`Error reading cache file ${key}:`, err); return { exists: false, error: true }; } } async function writeCache(key, data) { const filePath = path.join(CACHE_DIR, key); try { const content = JSON.stringify(data); await fs.writeFile(filePath, content, 'utf-8'); } catch (err) { console.error(`Error writing cache file ${key}:`, err); } } app.get('/author/:identifier', async (req, res) => { const identifier = req.params.identifier; let page = req.query.page; let delay = req.query.delay; let pageNum = null; if (page !== undefined) { const parsedPage = parseInt(page, 10); if (!isNaN(parsedPage) && parsedPage >= 0) { pageNum = parsedPage; } else if (page === 'null' || page === '') { pageNum = null; } else { return res.status(400).json({ status: 'error', error: { code: 400, message: 'Invalid page parameter. Use null, 0, or a positive integer.' } }); } } let delayMs = undefined; if (delay !== undefined) { const parsedDelay = parseInt(delay, 10); if (!isNaN(parsedDelay) && parsedDelay >= 0) { delayMs = parsedDelay; } else { return res.status(400).json({ status: 'error', error: { code: 400, message: 'Invalid delay parameter. Use a non-negative integer.' } }); } } const cacheKey = generateCacheKey('author', identifier, { page: pageNum }); let cacheEntry = null; try { cacheEntry = await readCache(cacheKey); if (cacheEntry.exists && !cacheEntry.isStale) { return res.json(cacheEntry.data); } const freshResult = await getAuthorData(identifier, pageNum, delayMs); if (freshResult.status === 'success') { await writeCache(cacheKey, freshResult); return res.json(freshResult); } else { if (cacheEntry.exists) { return res.json(cacheEntry.data); } else { return res.status(freshResult.error.code >= 400 && freshResult.error.code < 600 ? freshResult.error.code : 500).json(freshResult); } } } catch (err) { if (cacheEntry && cacheEntry.exists) { return res.json(cacheEntry.data); } else { console.error(`Error processing /author/${identifier} (CacheKey: ${cacheKey}):`, err); return res.status(500).json({ status: 'error', error: { code: 500, message: 'Internal Server Error', originalMessage: err.message } }); } } }); app.get('/author/:identifier/filters', async (req, res) => { const identifier = req.params.identifier; const cacheKey = generateCacheKey('filters', identifier); let cacheEntry = null; try { cacheEntry = await readCache(cacheKey); if (cacheEntry.exists && !cacheEntry.isStale) { return res.json(cacheEntry.data); } const freshResult = await getAuthorFilters(identifier); if (freshResult.status === 'success') { await writeCache(cacheKey, freshResult); return res.json(freshResult); } else { if (cacheEntry.exists) { return res.json(cacheEntry.data); } else { return res.status(freshResult.error.code >= 400 && freshResult.error.code < 600 ? freshResult.error.code : 500).json(freshResult); } } } catch (err) { if (cacheEntry && cacheEntry.exists) { return res.json(cacheEntry.data); } else { console.error(`Error processing /author/${identifier}/filters (CacheKey: ${cacheKey}):`, err); return res.status(500).json({ status: 'error', error: { code: 500, message: 'Internal Server Error', originalMessage: err.message } }); } } }); app.get('/', (req, res) => { res.redirect('/docs'); }); // No need to ensure cache dir here app.listen(port, () => { console.log(`StihiRus API wrapper listening on port ${port}`); console.log(`Cache files will be stored in: ${CACHE_DIR}`); console.log(`API Docs available at /docs`); });