import common from '../../../../common'; import _ from 'lodash'; import { langCodes } from '../../libs/api-v3/i18n'; import Bluebird from 'bluebird'; import fsCallback from 'fs'; import path from 'path'; import logger from '../../libs/api-v3/logger'; // Transform fs methods that accept callbacks in ones that return promises const fs = { readFile: Bluebird.promisify(fsCallback.readFile, {context: fsCallback}), writeFile: Bluebird.promisify(fsCallback.writeFile, {context: fsCallback}), stat: Bluebird.promisify(fsCallback.stat, {context: fsCallback}), mkdir: Bluebird.promisify(fsCallback.mkdir, {context: fsCallback}), }; let api = {}; function walkContent (obj, lang) { _.each(obj, (item, key, source) => { if (_.isPlainObject(item) || _.isArray(item)) return walkContent(item, lang); if (_.isFunction(item) && item.i18nLangFunc) source[key] = item(lang); }); } // After the getContent route is called the first time for a certain language // the response is saved on disk and subsequentially served directly from there to reduce computation. // Example: if `cachedContentResponses.en` is true it means that the response is cached let cachedContentResponses = {}; // Language key set to true while the cache file is being written let cacheBeingWritten = {}; _.each(langCodes, code => { cachedContentResponses[code] = false; cacheBeingWritten[code] = false; }); const CONTENT_CACHE_PATH = path.join(__dirname, '/../../../build/content_cache/'); async function saveContentToDisk (language, content) { try { cacheBeingWritten[language] = true; await fs.stat(CONTENT_CACHE_PATH); // check if the directory exists, if it doesn't an error is thrown await fs.writeFile(`${CONTENT_CACHE_PATH}${language}.json`, content, 'utf8'); cacheBeingWritten[language] = false; cachedContentResponses[language] = true; } catch (err) { if (err.code === 'ENOENT' && err.syscall === 'stat') { // the directory doesn't exists, create it and retry await fs.mkdir(CONTENT_CACHE_PATH); return saveContentToDisk(language, content); } else { cacheBeingWritten[language] = false; logger.error(err); return; } } } /** * @api {get} /api/v3/content Get all available content objects * @apiDescription Does not require authentication. * @apiVersion 3.0.0 * @apiName ContentGet * @apiGroup Content * * @apiParam {string} language Query parameter, the language code used for the items' strings. Defaulting to english * * @apiSuccess {Object} data All the content available on Habitica */ api.getContent = { method: 'GET', url: '/content', async handler (req, res) { let language = 'en'; let proposedLang = req.query.language && req.query.language.toString(); if (proposedLang in cachedContentResponses) { language = proposedLang; } let content; // is the content response for this language cached? if (cachedContentResponses[language] === true) { content = await fs.readFile(`${CONTENT_CACHE_PATH}${language}.json`, 'utf8'); } else { // generate the response content = _.cloneDeep(common.content); walkContent(content, language); content = JSON.stringify(content); } res.set({ 'Content-Type': 'application/json', }); let jsonResString = `{"success": true, "data": ${content}}`; res.status(200).send(jsonResString); // save the file in background unless it's already cached or being written right now if (cachedContentResponses[language] !== true && cacheBeingWritten[language] !== true) { saveContentToDisk(language, content); } }, }; module.exports = api;