Reworked webserver to eliminate redundant code

This commit is contained in:
Carlo Morgenstern 2021-10-24 15:49:18 +02:00
parent 73b396c05e
commit 3ebbb867b2

216
index.js
View file

@ -6,146 +6,136 @@ const fsStore = require('cache-manager-fs-binary');
const { CardCreator } = require('./create-card'); const { CardCreator } = require('./create-card');
const port = process.env.PORT || 5000; const port = process.env.PORT || 5000;
const xivApiKey = typeof process.env.XIV_API_KEY === 'string' && process.env.XIV_API_KEY !== '' ? process.env.XIV_API_KEY : undefined;
const supportedLanguages = ['en', 'ja', 'de', 'fr'];
const app = express(); const app = express();
const creator = new CardCreator(); const creator = new CardCreator();
// Initialize caching on disk // Initialize caching on disk
const diskCache = cacheManager.caching({ const diskCache = cacheManager.caching({
store: fsStore, store: fsStore,
options: { options: {
reviveBuffers: true, reviveBuffers: true,
binaryAsStream: false, binaryAsStream: false,
ttl: 60 * 60 * 4 /* seconds */, ttl: 14400, // s = 4h
maxsize: 1000 * 1000 * 1000 /* max size in bytes on disk */, maxsize: 1000000000, // bytes = 1 GB
path: 'diskcache', path: 'diskcache',
preventfill: true preventfill: true,
} }
}); });
async function getCharIdByName(world, name, retries = 1) { async function getCharacterIdByName(world, name, retries = 1) {
if (retries === -1) return undefined; if (retries === -1) return undefined;
const response = await fetch(`https://xivapi.com/character/search?name=${name}&server=${world}`); const searchUrl = new URL('https://xivapi.com/character/search');
const data = await response.json(); searchUrl.searchParams.set('name', name)
searchUrl.searchParams.set('server', world)
if (xivApiKey != null) searchUrl.searchParams.set('private_key', xivApiKey)
if (data.Results[0] === undefined) const response = await fetch(searchUrl.toString());
return getCharIdByName(world, name, --retries); const data = await response.json();
return data.Results[0].ID; if (data.Results[0] === undefined) return getCharacterIdByName(world, name, --retries);
return data.Results[0].ID;
} }
app.get('/prepare/id/:charaId', async (req, res) => { async function cacheCreateCard(characterId, customImage, language) {
var cacheKey = `img:${req.params.charaId}`; const cacheKey = `img:${characterId}:${customImage}:${language}`;
var ttl = 60 * 60 * 4; // 4 hours
diskCache.wrap(cacheKey, return diskCache.wrap(cacheKey, async () => {
// called if the cache misses in order to generate the value to cache await creator.ensureInit().catch(error => { throw new Error(`Init failed with: ${error}`) });
function (cb) { const image = await creator.createCard(characterId, customImage, language).catch(error => { throw new Error(`Create card failed with: ${error}`) });
creator.ensureInit().then(() => creator.createCard(req.params.charaId), (reason) => cb('Init failed: ' + reason, null)).then(image => cb(null, {
binary: {
image: image,
}
})).catch((reason) => cb('createCard failed: ' + reason, null));
},
// Options, see node-cache-manager for more examples
{ ttl: ttl },
function (err, result) {
if (err !== null) {
console.error(err);
res.status(500).send({status: "error", reason: err});
return;
}
res.status(200).send({status: "ok", url: `/characters/id/${req.params.charaId}.png`}); return {
} binary: {
); image,
}) },
};
});
}
app.get('/prepare/name/:world/:charName', async (req, res) => { function getOriginalQueryString(req) {
var id = await getCharIdByName(req.params.world, req.params.charName); const url = new URL(req.originalUrl, 'http://example.org');
return url.search;
}
if (id === undefined) { app.get('/prepare/id/:characterId', (req, res, next) => {
res.status(404).send({status: "error", reason: "Character not found."}); const language = typeof req.query.lang === 'string' && supportedLanguages.includes(req.query.lang) ? req.query.lang : supportedLanguages[0];
return;
}
res.redirect(`/prepare/id/${id}`); cacheCreateCard(req.params.characterId, null, language)
}) .then(() => {
res.status(200).json({
status: 'ok',
url: `/characters/id/${req.params.characterId}.png`,
});
})
.catch(next);
});
app.get('/characters/id/:charaId.png', async (req, res) => { app.get('/prepare/name/:world/:characterName', (req, res, next) => {
var cacheKey = `img:${req.params.charaId}`; getCharacterIdByName(req.params.world, req.params.characterName)
var ttl = 60 * 60 * 4; // 4 hours .then(characterId => {
if (characterId == null) {
res.status(404).send({ status: 'error', reason: 'Character not found.' });
} else {
res.redirect(`/prepare/id/${characterId}${getOriginalQueryString(req)}`);
}
})
.catch(next);
});
diskCache.wrap(cacheKey, app.get('/characters/id/:characterId.png', (req, res, next) => {
// called if the cache misses in order to generate the value to cache const language = typeof req.query.lang === 'string' && supportedLanguages.includes(req.query.lang) ? req.query.lang : supportedLanguages[0];
function (cb) {
creator.ensureInit().then(() => creator.createCard(req.params.charaId), (reason) => cb('Init failed: ' + reason, null)).then(image => cb(null, {
binary: {
image: image,
}
})).catch((reason) => cb('createCard failed: ' + reason, null));
},
// Options, see node-cache-manager for more examples
{ ttl: ttl },
function (err, result) {
if (err !== null) {
console.error(err);
res.status(500).send({status: "error", reason: err});
return;
}
var image = result.binary.image; cacheCreateCard(req.params.characterId, null, language)
.then(result => {
const image = result.binary.image;
res.writeHead(200, { res.writeHead(200, {
'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=14400',
'Content-Length': image.length, 'Content-Length': Buffer.byteLength(image),
'Cache-Control': 'public, max-age=14400' 'Content-Type': 'image/png',
}); });
res.end(image, 'binary'); res.end(image, 'binary');
})
.catch(next);
});
var usedStreams = ['image']; app.get('/characters/id/:characterId', (req, res) => {
// you have to do the work to close the unused files res.redirect(`/characters/id/${req.params.characterId}.png${getOriginalQueryString(req)}`);
// to prevent file descriptors leak });
for (var key in result.binary) {
if (!result.binary.hasOwnProperty(key)) continue;
if (usedStreams.indexOf(key) < 0
&& result.binary[key] instanceof Stream.Readable) {
if (typeof result.binary[key].close === 'function') {
result.binary[key].close(); // close the stream (fs has it)
} else {
result.binary[key].resume(); // resume to the end and close
}
}
}
}
);
})
app.get('/characters/id/:charaId', async (req, res) => { app.get('/characters/name/:world/:characterName.png', (req, res, next) => {
res.redirect(`/characters/id/${req.params.charaId}.png`); getCharacterIdByName(req.params.world, req.params.characterName)
}) .then(characterId => {
if (characterId == null) {
res.status(404).send({ status: 'error', reason: 'Character not found.' });
} else {
res.redirect(`/characters/id/${characterId}${getOriginalQueryString(req)}`);
}
})
.catch(next);
});
app.get('/characters/name/:world/:charName.png', async (req, res) => { app.get('/characters/name/:world/:characterName', (req, res) => {
var id = await getCharIdByName(req.params.world, req.params.charName); res.redirect(`/characters/name/${req.params.world}/${req.params.characterName}.png${getOriginalQueryString(req)}`);
});
if (id === undefined) {
res.status(404).send({status: "error", reason: "Character not found."});
return;
}
res.redirect(`/characters/id/${id}.png`);
})
app.get('/characters/name/:world/:charName', async (req, res) => {
res.redirect(`/characters/name/${req.params.world}/${req.params.charName}.png`);
})
app.get('/', async (req, res) => { app.get('/', async (req, res) => {
res.redirect('https://github.com/ArcaneDisgea/XIV-Character-Cards'); res.redirect('https://github.com/xivapi/XIV-Character-Cards');
}) });
app.use((error, req, res, next) => {
console.error(error);
res.status(500).json({
status: 'error',
reason: error instanceof Error ? error.stack : String(error),
});
});
app.listen(port, () => { app.listen(port, () => {
console.log(`Listening at http://localhost:${port}`) console.log(`Listening at http://localhost:${port}`);
}) });