mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-19 07:37:25 +01:00
v3: port getContent route caching responses to disk
This commit is contained in:
25
test/api/v3/integration/content/GET-content.test.js
Normal file
25
test/api/v3/integration/content/GET-content.test.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import {
|
||||||
|
requester,
|
||||||
|
translate as t,
|
||||||
|
} from '../../../../helpers/api-v3-integration.helper';
|
||||||
|
import i18n from '../../../../../common/script/i18n';
|
||||||
|
|
||||||
|
describe('GET /content', () => {
|
||||||
|
it('returns content (and does not require authentication)', async () => {
|
||||||
|
let res = await requester().get('/content');
|
||||||
|
expect(res).to.have.deep.property('backgrounds.backgrounds062014.beach');
|
||||||
|
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(t('backgroundBeachText'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns content not in English', async () => {
|
||||||
|
let res = await requester().get('/content?language=de');
|
||||||
|
expect(res).to.have.deep.property('backgrounds.backgrounds062014.beach');
|
||||||
|
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(i18n.t('backgroundBeachText', 'de'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to English if the desired language is not found', async () => {
|
||||||
|
let res = await requester().get('/content?language=wrong');
|
||||||
|
expect(res).to.have.deep.property('backgrounds.backgrounds062014.beach');
|
||||||
|
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(t('backgroundBeachText'));
|
||||||
|
});
|
||||||
|
});
|
||||||
106
website/src/controllers/api-v3/content.js
Normal file
106
website/src/controllers/api-v3/content.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import common from '../../../../common';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { langCodes } from '../../libs/api-v3/i18n';
|
||||||
|
import Q from 'q';
|
||||||
|
import fsCallback from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// Transform fs methods that accept callbacks in ones that return promises
|
||||||
|
const fs = {
|
||||||
|
readFile: Q.denodeify(fsCallback.readFile),
|
||||||
|
writeFile: Q.denodeify(fsCallback.writeFile),
|
||||||
|
stat: Q.denodeify(fsCallback.stat),
|
||||||
|
mkdir: Q.denodeify(fsCallback.mkdir),
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
// TODO log error
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /content Get all available content objects. Does not require authentication.
|
||||||
|
* @apiVersion 3.0.0
|
||||||
|
* @apiName ContentGet
|
||||||
|
* @apiGroup Content
|
||||||
|
*
|
||||||
|
* @apiParam {string} language Optional query parameter, the language code used for the items' strings. Defaulting to english
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object} content 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',
|
||||||
|
});
|
||||||
|
res.status(200).send(content);
|
||||||
|
|
||||||
|
// 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;
|
||||||
Reference in New Issue
Block a user