sandboxApp.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  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',
  138. checkBodyPropsExist('title'),
  139. function(req, res) {
  140. const { title } = req.body;
  141. const repoSlug = slug(title.toLowerCase());
  142. const existingRepo = exStore.getRepo(repoSlug);
  143. // Prevent duplicate title
  144. if(existingRepo) {
  145. return res.status(409).send("La collection '" + title + "' existe déjà !");
  146. }
  147. // Prepare config
  148. var config = Object.assign({
  149. title,
  150. examples: []
  151. }, repoTmpl);
  152. exStore.addRepository(repoSlug, config)
  153. .then(repo => res.json(config));
  154. }
  155. );
  156. app.post('/:repoSlug/examples/:exampleSlug/files',
  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. type: fileExt,
  195. // path: fullPath,
  196. content: fileContent
  197. })
  198. )
  199. .catch(err => {
  200. const statusCode = err.message.startsWith('Le fichier') ? 409 : 500;
  201. return res.status(statusCode).send(err.message);
  202. });
  203. }
  204. );
  205. /**
  206. * Create a new example for specified repo
  207. */
  208. app.post('/:repoSlug/examples',
  209. checkRepoExists,
  210. checkBodyPropsExist('title'),
  211. function(req, res) {
  212. const { title } = req.body;
  213. const { repoSlug } = req.params;
  214. // Prevent duplicate title
  215. var existingTitle = _.find(req.repo.examples, { title: title });
  216. if(existingTitle) {
  217. return res.status(409).send("L'exemple '" + title + "' existe déjà !");
  218. }
  219. var exampleSlug = slug(req.body.title.toLowerCase());
  220. // Prepare config
  221. var config = Object.assign({
  222. slug: exampleSlug,
  223. title,
  224. category: req.repo.defaultCategory
  225. }, exampleTmpl);
  226. // Prepare files to write
  227. var targetDir = examplesDir + '/' + repoSlug + '/' + exampleSlug;
  228. var files = mapObjToArray({
  229. 'example.html': '<!-- ' + title + ' -->\n',
  230. 'script.js': '// ' + title,
  231. 'config.json': beautify(config, null, 2, 100)
  232. }, 'file', 'content');
  233. fs.mkdirAsync(targetDir)
  234. .then(() => Promise.map(
  235. files, ({ file, content }) => fs.writeFileAsync(targetDir + '/' + file, content)
  236. ))
  237. .then(files => req.repo.examples.push(config))
  238. .then(() => res.json(config));
  239. }
  240. );
  241. app.get('/examples/:repoSlug/:slug',
  242. checkRepoExists,
  243. function(req, res) {
  244. const { repoSlug, slug } = req.params;
  245. var example = _.find(req.repo.examples, { slug });
  246. const { title, html, js, css, libsCss, libsJs } = example;
  247. console.log(example, title, html, js, css, libsCss, libsJs);
  248. readFileAsync(examplesDir + '/' + repoSlug + '/' + slug + '/example.html')
  249. .then(body =>
  250. Mustache.render(sandboxTpml, {
  251. body, repoSlug, slug, title, js, css, libsCss, libsJs,
  252. examplesDir: path.relative(__dirname, examplesDir)
  253. })
  254. )
  255. .then(html => res.send(html))
  256. .catch(err => {
  257. console.error(err);
  258. res.status(500).send('Error: ' + err.message);
  259. });
  260. }
  261. );
  262. app.get('/menu', (rea, res) => {
  263. res.send(exStore.getMenu());
  264. });
  265. app.get('/list/:repoPath', function(req, res) {
  266. const { repoPath } = req.params;
  267. const repo = exStore.getList(repoPath);
  268. if(! repo) {
  269. return res.status(404).send('Repo ' + repoPath + ' not found');
  270. }
  271. console.log('found repo', repo);
  272. const data = repo.examples.map(e => (
  273. { slug: e.slug, title: e.title }
  274. ));
  275. res.json(data);
  276. });
  277. app.put('/examples/:slug', function(req, res) {
  278. var slug = req.params.slug;
  279. var existing = _.find(examples, { slug: slug });
  280. if(! existing) {
  281. res.status(404).send("L'exemple avec l'identifiant '" + slug + "' est introuvable !");
  282. }
  283. var targetDir = __dirname + '/exemples/' + slug;
  284. if(req.body.html) {
  285. fs.writeFileSync(targetDir + '/contenu.html', req.body.html);
  286. }
  287. if(req.body.javascript) {
  288. fs.writeFileSync(targetDir + '/script.js', req.body.javascript);
  289. }
  290. var theDate = new Date();
  291. console.log(theDate.getHours() + ':' + theDate.getMinutes() + " - Sauvegarde de l'exemple '" + existing.title + " effectuée'");
  292. res.json({ success: true });
  293. });
  294. module.exports = app;