Spaces:
Sleeping
Sleeping
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`); | |
}); |