sandboxApp.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. /* global __dirname */
  2. /* jshint strict:false */
  3. "use strict";
  4. var express = require('express');
  5. var bodyParser = require('body-parser');
  6. var slug = require('slug');
  7. var beautify = require('json-beautify');
  8. var _ = require('lodash');
  9. var path = require('path');
  10. var Mustache = require('mustache');
  11. var app = express();
  12. var fs = require('fs');
  13. var Promise = require('bluebird'); Promise.promisifyAll(fs);
  14. var sandboxTpml = fs.readFileSync(__dirname + '/html/template.mustache.html').toString();
  15. var exampleTmpl = require('./lib/exampleTmpl.json');
  16. var repoTmpl = require('./lib/repoTmpl.json');
  17. var ExampleStore = require('./lib/ExampleStore');
  18. var resetExampleRepos = require('./lib/resetExampleRepos');
  19. var isTesting = process.env.NODE_ENV === 'testing';
  20. var examplesDir = ! isTesting ? __dirname + '/exemples' :
  21. __dirname + '/test/integration/test-examples';
  22. var exStore = new ExampleStore(examplesDir);
  23. var {
  24. readFileAsync,
  25. readFilesAsync
  26. } = require('./lib/fsio');
  27. var {
  28. getAcceptLanguage,
  29. getIndexBare,
  30. getIndexRepo,
  31. getIndexExample
  32. } = require('./lib/indexHandlers')(exStore, examplesDir, isTesting);
  33. var {
  34. // getIndexBare,
  35. getPartsRepo,
  36. getPartsExample
  37. } = require('./lib/partHandlers')(exStore, examplesDir);
  38. /**
  39. * Initialize example store
  40. */
  41. (function(doReset) {
  42. return doReset ? resetExampleRepos() : Promise.resolve(true);
  43. })(isTesting)
  44. .then(() => exStore.init());
  45. // .then(() => console.log(exStore.getMenu()));
  46. /**
  47. * Initialize Express app:
  48. * - root folder as static
  49. * - body parsers
  50. * - browser language detection middleware
  51. */
  52. app.use(express.static(__dirname));
  53. app.use(bodyParser.json());
  54. app.use(bodyParser.urlencoded({ extended: true }));
  55. app.use(getAcceptLanguage);
  56. function addExample(slug, title) {
  57. return fs.writeFileAsync(examplesJSON, beautify(examples, null, 2, 100));
  58. }
  59. function readConfigJson(exampleSlug) {
  60. console.log(exampleSlug);
  61. return require('./exemples/jquery/' + exampleSlug + '/config.json');
  62. }
  63. function mapObjToArray(obj, key, value) {
  64. var arr = [];
  65. for(var p in obj) {
  66. arr.push({
  67. [key]: p,
  68. [value]: obj[p]
  69. });
  70. }
  71. return arr;
  72. }
  73. function checkBodyPropsExist(props) {
  74. return function(req, res, next) {
  75. if(! props) {
  76. throw new Error('checkBodyPropsExist was called with empty props argument');
  77. }
  78. if(! req.body) {
  79. return res.status(400).send('request doest not have a body: please set "content-type" header to "application/json"');
  80. }
  81. props = typeof props === 'string' ? [props] : props;
  82. for(let p = 0 ; p < props.length ; p++) {
  83. const prop = props[p];
  84. if(! req.body[prop]) {
  85. return res.status(400).send('request body doest not have a `' + prop + '` parameter: please provide it.');
  86. }
  87. }
  88. next();
  89. };
  90. }
  91. function checkNameAndGetExt(filename) {
  92. const base = path.basename(filename);
  93. let ext = path.extname(filename);
  94. ext = ext ? ext.toLowerCase().substr(1) : ext;
  95. return base && ['html', 'js', 'css'].indexOf( ext ) > -1 ? ext : false;
  96. }
  97. /**
  98. * Get repo parts
  99. */
  100. app.get('/parts/:repoSlug', getPartsRepo);
  101. /**
  102. * Get example parts
  103. */
  104. app.get('/parts/:repoSlug/:exampleSlug', getPartsExample);
  105. /**
  106. * Index page: render with only repo list in menu
  107. */
  108. app.get('/', getIndexBare);
  109. /**
  110. * Index page: render for tests
  111. */
  112. // if(isTesting) {
  113. // app.get('/test', getIndexTest);
  114. // }
  115. /**
  116. * Repo page: render with repo list and selected repo's example list in menu
  117. */
  118. app.get('/:repoSlug', getIndexRepo);
  119. /**
  120. * Example page: render with repo list and selected repo's example list in menu,
  121. * and the editor with the selected example
  122. */
  123. app.get('/:repoSlug/:exampleSlug', getIndexExample);
  124. function checkRepoExists(req, res, next) {
  125. const { repoSlug } = req.params;
  126. console.log('### checkRepoExists', repoSlug);
  127. // Get repo from store
  128. req.repo = exStore.getRepo(repoSlug);
  129. if(! req.repo) {
  130. return res.status(404).send("Repo " + repoSlug + "not found");
  131. }
  132. next();
  133. }
  134. /**
  135. * Create a new repo
  136. */
  137. app.post('/repos', function(req, res) {
  138. // Check for title and extract params
  139. if(! req.body || ! req.body.title) {
  140. res.status(400).send('Le titre ne peut pas être vide !');
  141. }
  142. const { title } = req.body;
  143. const repoSlug = slug(title.toLowerCase());
  144. const existingRepo = exStore.getRepo(repoSlug);
  145. // Prevent duplicate title
  146. if(existingRepo) {
  147. return res.status(409).send("La collection '" + title + "' existe déjà !");
  148. }
  149. // Prepare config
  150. var config = Object.assign({
  151. title
  152. }, repoTmpl);
  153. exStore.addRepository(repoSlug, config)
  154. .then(repo => res.json(config));
  155. });
  156. app.post('/:repoSlug/examples/:exampleSlug/file',
  157. checkRepoExists,
  158. checkBodyPropsExist('name'),
  159. function(req, res) {
  160. const { name } = req.body;
  161. const { repoSlug, exampleSlug } = req.params;
  162. const re = /^[A-Za-z0-9_\-\.]+$/;
  163. const targetDir = examplesDir + '/' + repoSlug + '/' + exampleSlug;
  164. const fullPath = targetDir + '/' + name;
  165. const defaultContents = {
  166. html: '<!-- ' + name + ' -->',
  167. js: '// ' + name,
  168. css: '/* ' + name + ' */',
  169. }
  170. let fileExt;
  171. if(! re.test(name)) {
  172. return res.status(400).json('Le paramètre `name` est incorrect: caractères autorisés: lettres, chiffres, _, - et .' );
  173. }
  174. if( ! ( fileExt = checkNameAndGetExt( name ) ) ) {
  175. return res.status(400).json("Le paramètre `name` est incorrect: il doit comporter un nom suivi d'une extension (.html, .js ou .css)" );
  176. }
  177. const fileContent = defaultContents[fileExt];
  178. fs.statAsync(fullPath)
  179. // Invert the usual flow of a Promise. fs.stat() fails if file does not exist (which is what we want)
  180. // Hence .catch() is a success handler and .then() an error handler (has to rethrow)
  181. .then(stats => {
  182. throw new Error('Le fichier `' + name + '` existe déjà !');
  183. })
  184. .catch(err => {
  185. // Rethrow error if it is not a "file not found" thrown by fs.stat()
  186. if( ! err.message.startsWith('ENOENT') ) {
  187. throw err;
  188. }
  189. return fs.writeFileAsync(fullPath, fileContent);
  190. })
  191. .then(() => exStore.addExampleFile(repoSlug, exampleSlug, name))
  192. .then(() => res.json({
  193. name,
  194. path: fullPath,
  195. content: fileContent
  196. })
  197. )
  198. .catch(err => {
  199. const statusCode = err.message.startsWith('Le fichier') ? 409 : 500;
  200. return res.status(statusCode).send(err.message);
  201. });
  202. }
  203. );
  204. /**
  205. * Create a new example for specified repo
  206. */
  207. app.post('/:repoSlug/examples',
  208. checkRepoExists,
  209. checkBodyPropsExist('title'),
  210. function(req, res) {
  211. const { title } = req.body;
  212. const { repoSlug } = req.params;
  213. // Prevent duplicate title
  214. var existingTitle = _.find(req.repo.examples, { title: title });
  215. if(existingTitle) {
  216. return res.status(409).send("L'exemple '" + title + "' existe déjà !");
  217. }
  218. var exampleSlug = slug(req.body.title.toLowerCase());
  219. // Prepare config
  220. var config = Object.assign({
  221. slug: exampleSlug,
  222. title,
  223. category: req.repo.defaultCategory
  224. }, exampleTmpl);
  225. // Prepare files to write
  226. var targetDir = examplesDir + '/' + repoSlug + '/' + exampleSlug;
  227. var files = mapObjToArray({
  228. 'example.html': '<!-- ' + title + ' -->\n',
  229. 'script.js': '// ' + title,
  230. 'config.json': beautify(config, null, 2, 100)
  231. }, 'file', 'content');
  232. fs.mkdirAsync(targetDir)
  233. .then(() => Promise.map(
  234. files, ({ file, content }) => fs.writeFileAsync(targetDir + '/' + file, content)
  235. ))
  236. .then(files => req.repo.examples.push(config))
  237. .then(() => res.json(config));
  238. }
  239. );
  240. app.get('/examples/:repoSlug/:slug',
  241. checkRepoExists,
  242. function(req, res) {
  243. const { repoSlug, slug } = req.params;
  244. var example = _.find(req.repo.examples, { slug });
  245. const { title, html, js, css, libsCss, libsJs } = example;
  246. console.log(example, title, html, js, css, libsCss, libsJs);
  247. readFileAsync(__dirname + '/exemples/' + repoSlug + '/' + slug + '/example.html')
  248. .then(body =>
  249. Mustache.render(sandboxTpml, { body, repoSlug, slug, title, js, css, libsCss, libsJs })
  250. )
  251. .then(html => res.send(html));
  252. }
  253. );
  254. app.get('/menu', (rea, res) => {
  255. res.send(exStore.getMenu());
  256. });
  257. app.get('/list/:repoPath', function(req, res) {
  258. const { repoPath } = req.params;
  259. const repo = exStore.getList(repoPath);
  260. if(! repo) {
  261. return res.status(404).send('Repo ' + repoPath + ' not found');
  262. }
  263. console.log('found repo', repo);
  264. const data = repo.examples.map(e => (
  265. { slug: e.slug, title: e.title }
  266. ));
  267. res.json(data);
  268. });
  269. app.put('/examples/:slug', function(req, res) {
  270. var slug = req.params.slug;
  271. var existing = _.find(examples, { slug: slug });
  272. if(! existing) {
  273. res.status(404).send("L'exemple avec l'identifiant '" + slug + "' est introuvable !");
  274. }
  275. var targetDir = __dirname + '/exemples/' + slug;
  276. if(req.body.html) {
  277. fs.writeFileSync(targetDir + '/contenu.html', req.body.html);
  278. }
  279. if(req.body.javascript) {
  280. fs.writeFileSync(targetDir + '/script.js', req.body.javascript);
  281. }
  282. var theDate = new Date();
  283. console.log(theDate.getHours() + ':' + theDate.getMinutes() + " - Sauvegarde de l'exemple '" + existing.title + " effectuée'");
  284. res.json({ success: true });
  285. });
  286. module.exports = app;