sandboxApp.js 9.1 KB

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