1 |
|
2 | "use strict";
|
3 |
|
4 | var path = require('path')
|
5 | , basename = path.basename(__filename, '.js')
|
6 | , debug = require('debug')('castor:' + basename)
|
7 | , fs = require('fs')
|
8 | , kuler = require('kuler')
|
9 | , mime = require('mime')
|
10 | , express = require('express')
|
11 | , Loader = require('castor-load')
|
12 | , Computer = require('./helpers/compute.js')
|
13 | , Download = require('./helpers/download.js')
|
14 | , serveStatic = require('serve-static')
|
15 | , I18n = require('i18n-2')
|
16 | , Hook = require('./helpers/hook.js')
|
17 | , async = require('async')
|
18 | , MongoClient = require('mongodb').MongoClient
|
19 | , JBJ = require('jbj')
|
20 | , ACL = require('./helpers/acl.js')
|
21 | , passport = require('passport')
|
22 | , Errors = require('./helpers/errors.js')
|
23 | , Agent = require('./helpers/agent.js')
|
24 | , querystring = require('querystring')
|
25 | , datamodel = require('datamodel')
|
26 | , objectPath = require("object-path")
|
27 | ;
|
28 |
|
29 | module.exports = function(config, online) {
|
30 |
|
31 | var core = {
|
32 | server : undefined,
|
33 | app : express(),
|
34 | jbj: JBJ,
|
35 | states : { },
|
36 | config : config,
|
37 | models : {},
|
38 | connect : undefined,
|
39 | computer : undefined,
|
40 | loaders : undefined,
|
41 | downloaders : undefined,
|
42 | heart: undefined,
|
43 | agent : new Agent(config.get('port')),
|
44 | passport : passport,
|
45 | acl : new ACL(),
|
46 | Errors : Errors
|
47 | };
|
48 |
|
49 |
|
50 |
|
51 |
|
52 | core.passport.serializeUser(function(user, done) {
|
53 | done(null, JSON.stringify(user));
|
54 | });
|
55 | core.passport.deserializeUser(function(user, done) {
|
56 | done(null, JSON.parse(user));
|
57 | });
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 | var extensionPath, viewPath;
|
65 | try {
|
66 | extensionPath = require('./helpers/view.js')(config);
|
67 | console.info(kuler('Set extension directory. ', 'olive'), kuler(extensionPath, 'limegreen'));
|
68 | viewPath = path.resolve(extensionPath, './views')
|
69 | if (!fs.existsSync(viewPath)) {
|
70 | viewPath = extensionPath;
|
71 | }
|
72 | }
|
73 | catch(e) {
|
74 | return online(e);
|
75 | }
|
76 |
|
77 |
|
78 |
|
79 |
|
80 | config.fix('connectionURI', 'mongodb://' + config.get('databaseHost') + '/' + config.get('databaseName'));
|
81 |
|
82 |
|
83 | var publicPath = [
|
84 | path.resolve(extensionPath, './public'),
|
85 | path.resolve(viewPath, './public'),
|
86 | path.resolve(__dirname, './public')
|
87 | ].filter(fs.existsSync).shift();
|
88 | if (publicPath === undefined) {
|
89 | return online(new Error('publicPath is not defined'));
|
90 | }
|
91 |
|
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 | var filters = new Hook('filters')
|
98 | filters.from(extensionPath, __dirname)
|
99 | filters.over(config.get('filters'))
|
100 | filters.apply(function(hash, func) {
|
101 | JBJ.use(func);
|
102 | });
|
103 |
|
104 |
|
105 |
|
106 |
|
107 | var protocols = new Hook('protocols')
|
108 | protocols.from(extensionPath, __dirname)
|
109 | protocols.over(config.get('protocols'))
|
110 | protocols.apply(function(hash, func, item) {
|
111 | JBJ.register(item.pattern || hash, func(item.options || {}, core));
|
112 | });
|
113 |
|
114 | JBJ.register('local:', require('./protocols/local.js')({}, core));
|
115 | JBJ.register('http:', require('./protocols/http.js')({}, core));
|
116 | JBJ.register('https:', require('./protocols/https.js')({}, core));
|
117 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 | var pulse;
|
127 | try {
|
128 | core.heart = require('./helpers/heart.js')(config.get('heartrate'));
|
129 | pulse = core.heart.newPulse();
|
130 | }
|
131 | catch(e) {
|
132 | return online(e);
|
133 | }
|
134 | var heartbeats = new Hook('heartbeats');
|
135 | heartbeats.from(extensionPath, __dirname)
|
136 | heartbeats.over(config.get('heartbeats'))
|
137 | heartbeats.apply(function(hash, func, item) {
|
138 | item.repeat = Number(item.repeat);
|
139 | item.beat = Number(item.beat);
|
140 | core.heart.createEvent(Number.isNaN(item.beat) ? 1 : item.beat, {repeat: Number.isNaN(item.repeat) ? 0 : item.repeat}, func(item.options, core));
|
141 | });
|
142 | core.heart.createEvent(2, {repeat: 1}, require('./heartbeats/compute.js')({}, core));
|
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 | core.models.init = require('./models/init.js')
|
150 | core.models.mongo = require('./models/mongo.js')
|
151 |
|
152 | core.models.getTable = require('./models/get-table.js')
|
153 | core.models.getDocument = require('./models/get-document.js')
|
154 | core.models.getDocuments = require('./models/get-documents.js')
|
155 | core.models.computeDocuments = require('./models/compute-documents.js')
|
156 |
|
157 | var models = new Hook('models');
|
158 | models.from(extensionPath, __dirname)
|
159 | models.over(config.get('models'))
|
160 | models.apply(function(hash, func, item) {
|
161 | core.models[hash] = func;
|
162 | });
|
163 |
|
164 |
|
165 |
|
166 |
|
167 |
|
168 | datamodel([core.models.mongo, core.models.init])
|
169 | .apply(core)
|
170 | .then(function(res) {
|
171 | if (res.initState) {
|
172 | console.info(kuler('Collections index initialized.', 'olive'), kuler(core.config.get('collectionNameIndex'), 'limegreen'));
|
173 | }
|
174 | }).catch(online);
|
175 |
|
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 | core.loaders = [];
|
182 | core.loaders.push(['**/*', require('./loaders/prepend.js'), {}]);
|
183 |
|
184 | var loaders = new Hook('loaders');
|
185 | loaders.from(extensionPath, __dirname)
|
186 | loaders.over(config.get('loaders'))
|
187 | loaders.apply(function(hash, func, item) {
|
188 | core.loaders.push([item.pattern || '**/*', func, item.options]);
|
189 | });
|
190 | core.loaders.push(['**/*', require('./loaders/document.js'), { stylesheet: config.get('documentFields') }]);
|
191 | core.loaders.push(['**/*', require('./loaders/append.js'), {}]);
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 | var ldr, ldropts;
|
198 | if (config.has('dataPath')) {
|
199 | objectPath.set(core.states, 'hotfolder.first.syncOver', false);
|
200 | try {
|
201 | ldropts = {
|
202 |
|
203 | "connexionURI" : config.get('connectionURI'),
|
204 | "collectionName": config.get('collectionNameHotFolder'),
|
205 | "concurrency" : config.get('concurrency'),
|
206 | "delay" : config.get('delay'),
|
207 | "maxFileSize" : config.get('maxFileSize'),
|
208 | "writeConcern" : config.get('writeConcern'),
|
209 | "ignore" : config.get('filesToIgnore')
|
210 | }
|
211 | ldr = new Loader(config.get('dataPath'), ldropts);
|
212 |
|
213 | if (fs.existsSync(config.get('dataPath'))) {
|
214 | console.info(kuler('Watching hot directory. ', 'olive'), kuler(config.get('dataPath'), 'limegreen'));
|
215 | core.loaders.forEach(function(loader) {
|
216 | ldr.use(loader[0], loader[1](loader[2], core));
|
217 | });
|
218 | ldr.sync(function(err) {
|
219 | if (err instanceof Error) {
|
220 | console.error(kuler("Loader synchronization failed.", "red"), kuler(err.toString(), 'orangered'));
|
221 | }
|
222 | else {
|
223 | console.info(kuler('Files and Database are synchronised.', 'olive'));
|
224 | }
|
225 | });
|
226 | config.set('collectionName', ldr.options.collectionName);
|
227 | }
|
228 | }
|
229 | catch(e) {
|
230 | return online(e);
|
231 | }
|
232 | }
|
233 |
|
234 |
|
235 |
|
236 |
|
237 | core.indexes = [];
|
238 | core.indexes.push({ '_wid': 1 });
|
239 | core.indexes.push({ '_text': 'text' });
|
240 | core.indexes.push({ 'state': 1 });
|
241 | core.connect = function() {
|
242 | return MongoClient.connect(config.get('connectionURI'));
|
243 | }
|
244 | try {
|
245 | core.connect()
|
246 | .then(function(db) {
|
247 | db.collection(config.get('collectionName'))
|
248 | .then(function(coll) {
|
249 | debug('indexes', coll);
|
250 | var usfs = config.get('documentFields');
|
251 | core.indexes = Object.keys(usfs)
|
252 | .filter(function(i) {
|
253 | return (i !== '$text') && (usfs[i].noindex !== true);
|
254 | })
|
255 | .map(function(i) {
|
256 | var j = {}; j[i.replace('$','')] = 1; return j;
|
257 | });
|
258 | async.map(core.indexes, function(i, cb) {
|
259 | coll.ensureIndex(i, { w: config.get('writeConcern')}, function(err, indexName) {
|
260 | if (err instanceof Error) {
|
261 | console.error(kuler("Unable to create the index.", "red"), kuler(err.toString(), 'orangered'));
|
262 | }
|
263 | else {
|
264 | console.info(kuler('Index added.', 'olive'), kuler(Object.keys(i)[0] + '/' + indexName, 'limegreen'));
|
265 | }
|
266 | cb(err, indexName);
|
267 | });
|
268 | }, function(e, ret) {
|
269 | if (e) {
|
270 | throw e;
|
271 | }
|
272 | db.close();
|
273 | });
|
274 | }).catch(function(e) {
|
275 | throw e;
|
276 | });
|
277 | }).catch(function(e) {
|
278 | throw e;
|
279 | });
|
280 | }
|
281 | catch(e) {
|
282 | return online(e);
|
283 | }
|
284 |
|
285 |
|
286 |
|
287 |
|
288 |
|
289 |
|
290 | var cptlock, cptopts;
|
291 | try {
|
292 | cptopts = {
|
293 | "port": config.get('port'),
|
294 | "connectionURI" : config.get('connectionURI'),
|
295 | "collectionName": config.get('collectionNameHotFields'),
|
296 | "concurrency" : config.get('concurrency')
|
297 | }
|
298 | core.computer = new Computer(config.get('computedFields'), cptopts) ;
|
299 |
|
300 | core.computer.use('count', require('./operators/count.js'));
|
301 | core.computer.use('catalog', require('./operators/catalog.js'));
|
302 | core.computer.use('distinct', require('./operators/distinct.js'));
|
303 | core.computer.use('ventilate', require('./operators/ventilate.js'));
|
304 | core.computer.use('total', require('./operators/total.js'));
|
305 | core.computer.use('graph', require('./operators/graph.js'));
|
306 | core.computer.use('groupby', require('./operators/groupby.js'));
|
307 | core.computer.use('merge', require('./operators/merge.js'));
|
308 | core.computer.use('labelize', require('./operators/labelize.js'));
|
309 | core.computer.use('max', require('./operators/max.js'));
|
310 | core.computer.use('stats', require('./operators/stats.js'));
|
311 |
|
312 | var operators = new Hook('operators')
|
313 | operators.from(extensionPath, __dirname)
|
314 | operators.over(config.get('operators'))
|
315 | operators.apply(function(hash, func) {
|
316 | core.computer.use(hash, func);
|
317 | });
|
318 | var cptfunc = function() {
|
319 | if (cptlock === undefined || cptlock === false) {
|
320 | cptlock = true;
|
321 | core.heart.createEvent(2, {repeat: 1}, function() {
|
322 | cptlock = false;
|
323 | core.computer.run(function(err) {
|
324 | if (err instanceof Error) {
|
325 | console.error(kuler("Unable to compute some fields.", "red"), kuler(err.toString(), 'orangered'));
|
326 | }
|
327 | else {
|
328 | console.info(kuler('Corpus fields computed.', 'olive'));
|
329 | objectPath.set(core.states, 'hotfolder.first.syncOver', true);
|
330 | }
|
331 | });
|
332 | });
|
333 | }
|
334 | };
|
335 | if (ldr !== undefined) {
|
336 | ldr.on('browseOver', function (found) {
|
337 | objectPath.set(core.states, 'hotfolder.last.browseOver', found);
|
338 | });
|
339 | ldr.on('watching', function (err, doc) {
|
340 | objectPath.set(core.states, 'hotfolder.last.watching', doc);
|
341 | cptfunc();
|
342 | });
|
343 | ldr.on('checked', function (err, doc) {
|
344 | objectPath.set(core.states, 'hotfolder.last.checked', doc);
|
345 | cptfunc();
|
346 | });
|
347 | ldr.on('cancelled', function (err, doc) {
|
348 | objectPath.set(core.states, 'hotfolder.last.cancelled', doc);
|
349 | cptfunc();
|
350 | });
|
351 | ldr.on('added', function (err, doc) {
|
352 | objectPath.set(core.states, 'hotfolder.last.added', doc);
|
353 | cptfunc();
|
354 | });
|
355 | ldr.on('changed', function (err, doc) {
|
356 | objectPath.set(core.states, 'hotfolder.last.changed', doc);
|
357 | cptfunc();
|
358 | });
|
359 | ldr.on('dropped', function (err, doc) {
|
360 | objectPath.set(core.states, 'hotfolder.last.dropped', doc);
|
361 | cptfunc();
|
362 | });
|
363 | ldr.on('saved', function (doc) {
|
364 | objectPath.set(core.states, 'hotfolder.last.saved', doc);
|
365 | cptfunc();
|
366 | });
|
367 |
|
368 | }
|
369 | }
|
370 | catch(e) {
|
371 | return online(e);
|
372 | }
|
373 |
|
374 |
|
375 |
|
376 | var mimeTypes = config.get('mimeTypes');
|
377 | debug('mimeTypes', mimeTypes);
|
378 | mime.define(mimeTypes);
|
379 |
|
380 |
|
381 |
|
382 |
|
383 |
|
384 |
|
385 |
|
386 | var dwllock, dwlopts;
|
387 | try {
|
388 |
|
389 | dwlopts = {
|
390 | "maxAge": config.get('maxAge'),
|
391 | "concurrency" : config.get('concurrency')
|
392 | }
|
393 | core.downloader = new Download(dwlopts) ;
|
394 | core.downloader.use('*', require('./downloaders/columns.js')({}, core));
|
395 | core.downloader.use('*', require('./downloaders/uri.js')({}, core));
|
396 | core.downloader.use('min', require('./downloaders/min.js')({
|
397 | uniqueValueWith : config.get('uniqueValueWith')
|
398 | }, core));
|
399 | core.downloader.use('dry', require('./downloaders/dry.js')({}, core));
|
400 |
|
401 | core.downloader.use('tsv', require('./downloaders/tsv.js')({}, core));
|
402 |
|
403 |
|
404 | var downloaders = new Hook('downloaders')
|
405 | downloaders.from(extensionPath, __dirname)
|
406 | downloaders.over(config.get('downloaders'))
|
407 | downloaders.apply(function(hash, func, item) {
|
408 | core.downloader.use(item.pattern || '*', func(item.options, core));
|
409 | });
|
410 |
|
411 | core.downloader.use('*', require('./downloaders/json.js')({}, core));
|
412 | }
|
413 | catch(e) {
|
414 | return online(e);
|
415 | }
|
416 |
|
417 |
|
418 |
|
419 |
|
420 |
|
421 |
|
422 |
|
423 |
|
424 | var strategies = new Hook('strategies')
|
425 | strategies.from(extensionPath, __dirname)
|
426 | strategies.over(config.get('strategies'))
|
427 | strategies.apply(function(hash, func, item) {
|
428 | core.passport.use(func(item.options, core));
|
429 | });
|
430 |
|
431 |
|
432 |
|
433 |
|
434 |
|
435 |
|
436 | var authorizations = new Hook('authorizations')
|
437 | authorizations.from(extensionPath, __dirname)
|
438 | authorizations.over(config.get('authorizations'))
|
439 | authorizations.apply(function(hash, func, item) {
|
440 | core.acl.use(item.pattern, func(item.options, core));
|
441 | });
|
442 | core.acl.use('* /**', require('./authorizations/recall.js')());
|
443 |
|
444 |
|
445 |
|
446 |
|
447 |
|
448 |
|
449 |
|
450 |
|
451 | if (config.get('trustProxy') === true) {
|
452 | core.app.enable('trust proxy');
|
453 | }
|
454 |
|
455 |
|
456 |
|
457 |
|
458 |
|
459 |
|
460 |
|
461 | try {
|
462 | core.app.use(function (req, res, next) {
|
463 | req.routeParams = {};
|
464 | req.config = config;
|
465 | req.core = core;
|
466 | next();
|
467 | });
|
468 | core.app.use(require('express-cache-response-directive')());
|
469 | core.app.use(require('morgan')(config.get('logFormat'), { stream : process.stderr }));
|
470 | core.app.use(require('serve-favicon')(path.resolve(publicPath, './favicon.ico')));
|
471 | core.app.use(require('cookie-parser')());
|
472 | core.app.use(require('express-session')({ secret: __dirname, resave: false, saveUninitialized: false }));
|
473 | core.app.use(passport.initialize());
|
474 | core.app.use(passport.session());
|
475 | I18n.expressBind(core.app, {
|
476 | locales: ['en', 'fr'],
|
477 | directory: path.resolve(extensionPath, './locales')
|
478 | });
|
479 | core.app.use(require('./middlewares/i18n.js')());
|
480 |
|
481 | var middlewares = new Hook('middlewares')
|
482 | middlewares.from(extensionPath, __dirname)
|
483 | middlewares.over(config.get('middlewares'))
|
484 | middlewares.apply(function(hash, func, item) {
|
485 | core.app.use(item.path || hash, func(item.options, core));
|
486 | });
|
487 | core.app.use(function (req, res, next) {
|
488 |
|
489 | res.resolveTemplate = function(templateName, depth) {
|
490 | return [
|
491 | path.resolve(viewPath, templateName),
|
492 | path.resolve(__dirname, './views/', templateName)
|
493 | ].slice(0, depth).filter(fs.existsSync).shift()
|
494 | }
|
495 | next();
|
496 | });
|
497 | }
|
498 | catch(e) {
|
499 | return online(e);
|
500 | }
|
501 |
|
502 |
|
503 |
|
504 |
|
505 |
|
506 |
|
507 |
|
508 | core.app.set('views', [viewPath, path.resolve(__dirname, './views/')]);
|
509 |
|
510 | core.app.engine('jbj', require('./engines/jbj.js')({}, core));
|
511 | core.app.engine('html', require('./engines/nunjucks.js')({views: core.app.get('views')}, core));
|
512 | core.app.engine('txt', require('./engines/rawfile.js')({}, core));
|
513 |
|
514 | var engines = new Hook('engines')
|
515 | engines.from(extensionPath, __dirname)
|
516 | engines.over(config.get('engines'))
|
517 | engines.apply(function(hash, func, item) {
|
518 | core.app.engine(item.pattern || hash, func(item.options || {}, core));
|
519 | });
|
520 | core.app.set('view engine', config.get('defaultEngine'));
|
521 |
|
522 |
|
523 |
|
524 |
|
525 | if (config.get('rootURL') !== undefined && config.get('rootURL') !== '/') {
|
526 | core.app.route('/').all(function(req, res) {
|
527 | res.redirect(config.get('rootURL'));
|
528 | });
|
529 | }
|
530 |
|
531 |
|
532 |
|
533 |
|
534 |
|
535 |
|
536 | core.app.route('/*').all(core.acl.route());
|
537 |
|
538 |
|
539 |
|
540 |
|
541 |
|
542 | core.app.use(serveStatic(publicPath, {
|
543 | maxAge: config.get('maxAge')
|
544 | }))
|
545 |
|
546 |
|
547 |
|
548 |
|
549 |
|
550 | var pageRouter = express.Router();
|
551 |
|
552 |
|
553 |
|
554 |
|
555 |
|
556 | var routes = new Hook('routes')
|
557 | routes.from(extensionPath, __dirname)
|
558 | routes.over(config.get('routes'))
|
559 | routes.apply(function(hash, func, item) {
|
560 | var router = express.Router();
|
561 | func(router, core)
|
562 | core.app.use(router);
|
563 | });
|
564 |
|
565 |
|
566 | var restRouter = express.Router();
|
567 | require('./routes/rest.js')(restRouter, core)
|
568 | core.app.use(restRouter);
|
569 |
|
570 |
|
571 |
|
572 |
|
573 |
|
574 | core.app.use(function(req, res, next) {
|
575 | debug('Error handler');
|
576 | next(new Errors.PageNotFound('Not Found'));
|
577 | });
|
578 |
|
579 |
|
580 |
|
581 |
|
582 |
|
583 |
|
584 | core.app.use(function errorsHandler(err, req, res, next) {
|
585 | var statusCode;
|
586 | if (res.headersSent === false) {
|
587 | if (err instanceof Errors.PageNotFound || err instanceof Errors.TableNotFound) {
|
588 | statusCode = 404;
|
589 | }
|
590 | else if (err instanceof Errors.InvalidParameters) {
|
591 | statusCode = 400;
|
592 | }
|
593 | else if (err instanceof Errors.Forbidden) {
|
594 | statusCode = 403;
|
595 | }
|
596 | else {
|
597 | statusCode = 500;
|
598 | }
|
599 | }
|
600 | if (req.user === undefined && statusCode === 403 && config.get('loginURL')) {
|
601 | res.redirect(config.get('loginURL') + '?' + querystring.stringify({ 'url' : req.originalUrl }));
|
602 | return;
|
603 | }
|
604 | res.status(statusCode);
|
605 | console.error(kuler("Route error for", "red"), req.originalUrl, kuler(statusCode + ' - ' + err.toString(), 'orangered'), ' from ', req.get('referer'));
|
606 | if (req.accepts('html')) {
|
607 | res.render('error.html', {
|
608 | code: statusCode,
|
609 | name: err.name,
|
610 | message: err.message,
|
611 | error: err
|
612 | });
|
613 | return;
|
614 | }
|
615 | if (req.accepts('json')) {
|
616 | res.send({
|
617 | code: statusCode,
|
618 | name: err.name,
|
619 | message: err.message,
|
620 | });
|
621 | return;
|
622 |
|
623 | }
|
624 | res.type('text').send(err.toString());
|
625 | });
|
626 |
|
627 |
|
628 |
|
629 |
|
630 |
|
631 |
|
632 | core.server = require('http').createServer(core.app)
|
633 | core.server.timeout = config.get('timeout');
|
634 | core.server.listen(config.get('port'), function() {
|
635 | online(null, core);
|
636 | });
|
637 |
|
638 | return core.server;
|
639 | }
|