├── temp └── .gitkeep ├── storage └── .gitignore ├── config ├── dev.json-dist ├── test.json ├── autoconfig.js └── config.js ├── static ├── img │ └── logo.png ├── css │ └── main.css └── js │ ├── bootstrap.min.js │ └── jquery.min.js ├── views ├── error.jade ├── layout.jade └── index.jade ├── AUTHORS ├── .gitignore ├── lib ├── log.js ├── auth.js ├── freighter.js ├── installers │ ├── bower.js │ └── npm.js ├── tracker.js └── job_processor.js ├── bin └── freight-server ├── .travis.yml ├── routes ├── bundle_delete.js ├── bundle_download.js ├── index.js └── freight.js ├── Gruntfile.js ├── .jshintrc ├── package.json ├── LICENSE ├── .jscsrc ├── CHANGELOG ├── README.md └── app.js /temp/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /storage/.gitignore: -------------------------------------------------------------------------------- 1 | *.tar.gz 2 | -------------------------------------------------------------------------------- /config/dev.json-dist: -------------------------------------------------------------------------------- 1 | { 2 | "password": "CHANGE_ME_PLEASE" 3 | } 4 | -------------------------------------------------------------------------------- /config/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "password":"test", 3 | "track": { 4 | "delay": 20000 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-freight/freight-server/HEAD/static/img/logo.png -------------------------------------------------------------------------------- /views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Vlad Filippov (http://vf.io) 2 | Stephen Wan (http://www.stephenwan.net) 3 | James Reggio (https://github.com/jamesreggio) 4 | -------------------------------------------------------------------------------- /static/css/main.css: -------------------------------------------------------------------------------- 1 | .intro h1{ 2 | float: left; 3 | } 4 | .intro { 5 | overflow: hidden; 6 | } 7 | 8 | .logo { 9 | float: right; 10 | width: 150px; 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dump.rdb 3 | npm-debug 4 | config/*.json 5 | !config/test.json 6 | test/fixtures/project1/node_modules 7 | test/fixtures/project2/node_modules 8 | test/fixtures/project2/bower_components 9 | /temp 10 | /storage 11 | -------------------------------------------------------------------------------- /lib/log.js: -------------------------------------------------------------------------------- 1 | var bunyan = require('bunyan'); 2 | 3 | module.exports = function (conf) { 4 | 5 | return bunyan.createLogger({ 6 | name: 'freight-server', 7 | stream: process.stdout, 8 | level: conf.get('log').level 9 | }); 10 | 11 | }; 12 | -------------------------------------------------------------------------------- /bin/freight-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var app = require('../app'); 3 | app.set('port', app.conf.get('port')); 4 | 5 | var server = app.listen(app.get('port'), '::', function() { 6 | app.log.info('Freight Server is now running port', server.address().port); 7 | }); 8 | -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | meta(http-equiv='refresh', content='20') 6 | link(rel='stylesheet', href='/static/css/bootstrap.min.css') 7 | link(rel='stylesheet', href='/static/css/main.css') 8 | body 9 | block content 10 | script(src='/static/js/jquery.min.js') 11 | script(src='/static/js/bootstrap.min.js') 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | 5 | services: 6 | - redis-server 7 | 8 | script: 9 | - npm install node-freight/freight#1788d251ac4 10 | - NODE_ENV=test LOG_LEVEL=debug npm start & 11 | - sleep 10 12 | - node_modules/.bin/grunt lint 13 | - git clone https://github.com/node-freight/freight.git && cd freight 14 | - git checkout 1788d251ac4ff776ad597d06e2841638fa51463b 15 | - npm i 16 | - node_modules/.bin/mocha test/*.js --reporter=spec 17 | -------------------------------------------------------------------------------- /routes/bundle_delete.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | module.exports = function (log, conf) { 5 | 6 | // TODO: refactor this view 7 | return function (req, res) { 8 | if (req.params.file) { 9 | fs.unlink( 10 | path.join(conf.get('storage'), req.params.file), 11 | function (err) { 12 | if (err) { 13 | log.error(err); 14 | } else { 15 | log.info('Bundle Deleted:', req.params.file); 16 | } 17 | res.redirect('/'); 18 | }); 19 | } else { 20 | res.send(404); 21 | } 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 'use strict'; 3 | 4 | var LINT_FILES = ['{bin/**/,config/**/,lib/**/,routes/**/,test/}*.js']; 5 | 6 | grunt.initConfig({ 7 | jshint: { 8 | files: LINT_FILES, 9 | options: { 10 | jshintrc: '.jshintrc' 11 | } 12 | }, 13 | jscs: { 14 | src: LINT_FILES, 15 | options: { 16 | config: '.jscsrc' 17 | } 18 | } 19 | }); 20 | 21 | grunt.registerTask('lint', 'lint all the things', [ 22 | 'jshint', 23 | 'jscs' 24 | ]); 25 | 26 | grunt.loadNpmTasks('grunt-contrib-jshint'); 27 | grunt.loadNpmTasks('grunt-jscs'); 28 | }; 29 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "esnext": true, 6 | "globalstrict": false, 7 | "immed": true, 8 | "indent": 2, 9 | "latedef": true, 10 | "noarg": true, 11 | "node": true, 12 | "quotmark": "single", 13 | "regexp": true, 14 | "newcap": false, 15 | "smarttabs": true, 16 | "strict": false, 17 | "sub": true, 18 | "trailing": true, 19 | "undef": true, 20 | "unused": "vars", 21 | "globals": { 22 | "define": false 23 | }, 24 | "passfail": false, 25 | "maxerr": 100, 26 | "forin": false, 27 | "white": false, 28 | "predef": [ 29 | "exports", 30 | "require", 31 | "process" 32 | ], 33 | "shadow": false, 34 | "asi": true, 35 | "-W079": true 36 | } 37 | -------------------------------------------------------------------------------- /lib/auth.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | 3 | module.exports = function (log, conf) { 4 | 5 | var hasPassword = function() { 6 | return conf.get('password') !== ''; 7 | }; 8 | 9 | var checkPassword = function(password) { 10 | return ! hasPassword() || password === conf.get('password'); 11 | }; 12 | 13 | var emptyMiddleware = function(req, res, next) { next(); }; 14 | 15 | var authMiddleware = express.basicAuth(function(username, password) { 16 | return checkPassword(password); 17 | }, '** Note: Password is your Freight Server password **. Username is blank'); 18 | 19 | return { 20 | checkPassword: checkPassword, 21 | middleware: (hasPassword() ? authMiddleware : emptyMiddleware) 22 | }; 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /routes/bundle_download.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | module.exports = function (log, conf) { 5 | 6 | return function (req, res) { 7 | var fileName = null; 8 | try { 9 | fileName = req.route.params[0]; 10 | } catch (e) {} 11 | 12 | log.debug('UI Bundle Download request', fileName); 13 | 14 | if (! fileName) { 15 | return res.send(404); 16 | } 17 | 18 | fileName = fileName.replace(/\//g, '_'); 19 | var filePath = path.join(conf.get('storage'), fileName); 20 | 21 | fs.exists(filePath, function (exists) { 22 | if (exists) { 23 | return res.sendfile(filePath); 24 | } else { 25 | return res.send(404); 26 | } 27 | }); 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /lib/freighter.js: -------------------------------------------------------------------------------- 1 | var mkdirp = require('mkdirp'); 2 | 3 | module.exports = function (log, conf, jobs) { 4 | function Freighter() { 5 | } 6 | 7 | Freighter.create = function (project, extra) { 8 | 9 | //TODO: refactor 10 | mkdirp(project.tempPath, function (err) { 11 | if (err) { 12 | log.error(err); 13 | } 14 | 15 | var installJob = jobs.create('install', { project: project, title: project.name }) 16 | .priority(extra.priority) 17 | .save(); 18 | 19 | installJob.on('promotion', function () { 20 | log.debug('Kue Job Promoted'); 21 | }); 22 | 23 | installJob.on('complete', function () { 24 | log.debug('Kue Job Promoted'); 25 | }); 26 | }); 27 | }; 28 | 29 | return Freighter; 30 | }; 31 | -------------------------------------------------------------------------------- /config/autoconfig.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var crypto = require('crypto'); 3 | 4 | module.exports = function (expectedConfigFile) { 5 | 6 | if (! fs.existsSync(expectedConfigFile)) { 7 | var buf = crypto.randomBytes(256); 8 | var hash = crypto.createHash('sha1').update(buf).digest('hex'); 9 | 10 | // TODO: refactor 11 | console.log('***** NOTICE ****** \n'); 12 | console.log('You are missing "' + expectedConfigFile + '"'); 13 | console.log('Creating a configuration automatically for you....'); 14 | console.log('Your Freight Server password is: \n'); 15 | console.log(hash); 16 | console.log('\n Use the password above to generate bundles.'); 17 | var devSampleFile = JSON.parse(fs.readFileSync(__dirname + '/dev.json-dist')); 18 | devSampleFile.password = hash; 19 | fs.writeFileSync(expectedConfigFile, JSON.stringify(devSampleFile), null, 4); 20 | } 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "freight-server", 3 | "version": "0.5.1", 4 | "scripts": { 5 | "start": "redis-server & node ./bin/freight-server | bunyan -o short" 6 | }, 7 | "license": "MIT", 8 | "main": "./bin/freight-server", 9 | "dependencies": { 10 | "archiver": "^0.15.1", 11 | "async": "0.9.0", 12 | "bluebird": "2.3.2", 13 | "body-parser": "1.0.0", 14 | "bower": "1.4.1", 15 | "bunyan": "0.22.3", 16 | "convict": "0.4.2", 17 | "cross-spawn-async": "^2.1.6", 18 | "express": "3.17.4", 19 | "filesize": "2.0.3", 20 | "freight": "0.5.2", 21 | "jade": "1.7.0", 22 | "kue": "0.8.8", 23 | "mkdirp": "0.5.0", 24 | "moment": "2.8.3", 25 | "npm": "2.15.6", 26 | "rimraf": "2.2.8", 27 | "serve-favicon": "2.1.4", 28 | "tar.gz": "0.1.1" 29 | }, 30 | "devDependencies": { 31 | "grunt": "^0.4.5", 32 | "grunt-cli": "^0.1.13", 33 | "grunt-contrib-jshint": "^0.10.0", 34 | "grunt-jscs": "^0.7.1" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/vladikoff/freight-server.git" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Vlad Filippov 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowKeywords": ["with", "eval"], 3 | "disallowKeywordsOnNewLine": ["else"], 4 | "disallowMultipleLineStrings": true, 5 | "disallowSpaceAfterObjectKeys": true, 6 | "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-"], 7 | "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], 8 | "maximumLineLength": 160, 9 | "requireCapitalizedConstructors": true, 10 | "requireCurlyBraces": ["for", "while", "do"], 11 | "requireLineFeedAtFileEnd": true, 12 | "requireSpaceAfterBinaryOperators": ["=", ",", "+", "-", "/", "*", "==", "===", "!=", "!=="], 13 | "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return"], 14 | "requireSpaceAfterPrefixUnaryOperators": ["~"], 15 | "requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="], 16 | "requireSpacesInConditionalExpression": true, 17 | "validateIndentation": 2, 18 | "validateLineBreaks": "LF", 19 | "validateQuoteMarks": true, 20 | "validateJSDoc": { 21 | "checkParamNames": true, 22 | "checkRedundantParams": true, 23 | "requireParamTypes": true 24 | }, 25 | "requireSpaceAfterPrefixUnaryOperators": ["!"] 26 | } 27 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | v0.5.1: 2 | date: 2015-04-13 3 | changes: 4 | - Added bundle transmission size `limit` option 5 | - Update bower to 1.4.1 6 | v0.5.0: 7 | date: 2014-09-21 8 | changes: 9 | - Portable configuration location for storage and other directories. 10 | - Bug fix with bundle downloading through the user interface. 11 | - Bug fix for empty bundles and errors. if Bower or NPM fail to install you get a failed job in the Kue log. 12 | v0.4.0: 13 | date: 2014-07-22 14 | changes: 15 | - 'freight track REPO BRANCH_NAME' is now 'freight track REPO --track-branch=BRANCH_NAME'. 16 | - Added 'freight track [--track-directory=PATH_TO_PACKAGE_JSON_FILES], allows to track repositories 17 | that have 'package.json' and 'bower.json' in subdirectories. 18 | v0.3.3: 19 | date: 2014-07-06 20 | changes: 21 | - Documentation and bug fixes. 22 | v0.3.2: 23 | date: 2014-06-23 24 | changes: 25 | - Bug fixes. 26 | v0.3.1: 27 | date: 2014-06-22 28 | changes: 29 | - Bug fixes. 30 | v0.3.0: 31 | date: 2014-06-22 32 | changes: 33 | - Adds repository tracking with `freight track`. 34 | - Removed `-p` password flag. 35 | - Added more docs. 36 | v0.1.0: 37 | date: 2014-05-19 38 | changes: 39 | - Initial release. 40 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | div.container 5 | div.jumbotron.intro 6 | h1 Freight Server 7 | img.logo(src='/static/img/logo.png', alt='Freight Server') 8 | hr 9 | p 10 | a.btn.btn-primary.btn-info(href='/freights/active') View Freight Queue 11 | a.btn.btn-primary.btn(href='http://github.com/vladikoff/freight') About Freight 12 | blockquote 13 | p 14 | | I have #{files.length} bundle(s) at the moment and I'm using around #{process.heap} of memory. 15 | small 16 | | Freight Server 17 | h1 Bundles 18 | table.table.table-striped.table-hover 19 | thead 20 | tr 21 | th Bundle Name 22 | th Created 23 | th Size 24 | th File 25 | tbody 26 | for file in files 27 | tr 28 | td 29 | strong=file.name 30 | td=file.details.ctime 31 | td=file.details.size 32 | td 33 | a.btn.btn-sm.btn-warning(type='button', href='/ui/delete/#{file.name}') Delete 34 | a.btn.btn-sm.btn-success(type='button', href='#{file.details.download}') Download 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Freight Server 2 | ### Learn more about [Freight](https://github.com/vladikoff/freight). 3 | 4 | ### Quick Server Setup 5 | * Install [Git](http://git-scm.com/) on the server machine. (i.e on Ubuntu: `sudo apt-get install git-core`). 6 | If you want to `freight track` repositories via SSH, then configure proper SSH keys such that the server is able to clone the repositories. 7 | * Install [Redis](http://redis.io/). OS X: `brew install redis` or Ubuntu `sudo apt-get install redis-server`. 8 | 9 | Start the server: 10 | ``` 11 | git clone https://github.com/vladikoff/freight-server.git && cd freight-server 12 | npm install 13 | npm start 14 | ``` 15 | 16 | Server will start on port `8872`. You should be able to navigate to the dashboard: 17 | 18 |  19 | 20 | ### Configure 21 | 22 | #### Password 23 | 24 | Freight Server automatically configures a password for you. You can change it by modifying the [dev.json](config/dev.json-dist) file. 25 | 26 | A password setting of `""` (empty string) indicates that the Freight Server will not require a password for any actions. 27 | 28 | #### Other Configuration 29 | 30 | See [config/config.js](config/config.js#L12) for available 31 | configuration options and environment variables. The Freight Server uses [node-convict](https://github.com/mozilla/node-convict) to manage configuration. 32 | 33 | ### Notes 34 | 35 | * **Do not run server as a root user**. It is not safe and Bower will also complain that you are running as sudo. 36 | 37 | ### Author 38 | 39 | | [](https://twitter.com/vladikoff "Follow @vladikoff on Twitter") | 40 | |---| 41 | | [Vlad Filippov](http://vf.io/) | 42 | 43 | 44 | ### Release History 45 | See the [CHANGELOG](CHANGELOG). 46 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var filesize = require('filesize'); 3 | var async = require('async'); 4 | var path = require('path'); 5 | var moment = require('moment'); 6 | 7 | module.exports = function (log, conf) { 8 | 9 | // TODO: refactor this view 10 | return function (req, res) { 11 | var memory = process.memoryUsage(); 12 | var storage = conf.get('storage'); 13 | 14 | var data = { 15 | title: 'Freight Server', 16 | process: { 17 | heap: filesize(memory.heapUsed) 18 | } 19 | }; 20 | 21 | fs.readdir(storage, function (err, files) { 22 | if (err) { 23 | log.error(err); 24 | throw err; 25 | } 26 | 27 | async.map(files, 28 | function (file, complete) { 29 | fs.stat(path.join(storage, file), function (err, stat) { 30 | stat.size = filesize(stat.size); 31 | stat.download = '/storage/' + file; 32 | stat.ctime = moment(stat.ctime).format('MMMM Do YYYY, h:mm:ss a'); 33 | if (file.indexOf('tar.gz') > -1) { 34 | complete(err, { name: file, details: stat }); 35 | } else { 36 | complete(err, {}); 37 | } 38 | }); 39 | }, 40 | function (err, results) { 41 | if (err) { 42 | log.error('Failed to get statistics on all bundle files'); 43 | throw err; 44 | } 45 | 46 | var bundles = results.filter(function (file) { 47 | return file.hasOwnProperty('name'); 48 | }); 49 | 50 | bundles.sort(function(a, b) { 51 | return b.details.mtime.getTime() - a.details.mtime.getTime(); 52 | }); 53 | 54 | data.files = bundles; 55 | res.render('index', data); 56 | }); 57 | }); 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var bodyParser = require('body-parser'); 3 | var path = require('path'); 4 | var conf = require('./config/config')(); 5 | var log = require('./lib/log')(conf); 6 | var kue = require('kue'); 7 | 8 | // TODO: move this? this is needed for the server to be able to track bundles 9 | process.env.FREIGHT_PASSWORD = conf.get('password'); 10 | 11 | var index = require('./routes/index')(log, conf); 12 | var bundleDelete = require('./routes/bundle_delete')(log, conf); 13 | var bundleDownload = require('./routes/bundle_download')(log, conf); 14 | var freightRoutes = require('./routes/freight')(log, conf); 15 | var freightAuth = require('./lib/auth')(log, conf); 16 | 17 | var app = express(); 18 | app.conf = conf; 19 | app.log = log; 20 | 21 | app.set('views', path.join(__dirname, 'views')); 22 | app.set('view engine', 'jade'); 23 | 24 | var bodyParserOptions = { 25 | limit: conf.get('limit') + 'kb' 26 | }; 27 | app.use(bodyParser.json(bodyParserOptions)); 28 | app.use(bodyParser.urlencoded(bodyParserOptions)); 29 | 30 | app.post('/freight/check', freightRoutes.check); 31 | app.post('/freight/download', freightRoutes.download); 32 | app.post('/freight/track', freightRoutes.track); 33 | 34 | app.use('/static', express.static(path.join(__dirname, 'static'))); 35 | app.get('/storage/*', freightAuth.middleware, bundleDownload); 36 | app.get('/', freightAuth.middleware, index); 37 | // TODO: temporary, quick way to add delete 38 | app.get('/ui/delete/:file', freightAuth.middleware, bundleDelete); 39 | app.use(freightAuth.middleware); 40 | app.use('/freights', kue.app); 41 | 42 | /// catch 404 and forward to error handler 43 | app.use(function (req, res, next) { 44 | 45 | var err = new Error('Not Found'); 46 | err.status = 404; 47 | next(err); 48 | }); 49 | 50 | if (app.get('env') === 'development') { 51 | app.use(function (err, req, res, next) { 52 | res.status(err.status || 500); 53 | res.render('error', { 54 | message: err.message, 55 | error: err 56 | }); 57 | }); 58 | } 59 | 60 | app.use(function (err, req, res, next) { 61 | res.status(err.status || 500); 62 | res.render('error', { 63 | message: err.message, 64 | error: {} 65 | }); 66 | }); 67 | 68 | module.exports = app; 69 | -------------------------------------------------------------------------------- /lib/installers/bower.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var spawn = require('cross-spawn-async'); 3 | var path = require('path'); 4 | var Promise = require('bluebird'); 5 | 6 | module.exports = function(log) { 7 | 8 | var install = function (project, options) { 9 | options = options || {}; 10 | return new Promise(function(resolve, reject){ 11 | 12 | var bowerJson = JSON.stringify({ 13 | dependencies: project.bower.dependencies, 14 | devDependencies: options.production ? [] : project.bower.devDependencies, 15 | resolutions: project.bower.resolutions, 16 | name: project.name 17 | }, null, 4); 18 | var errorLog = ''; 19 | 20 | log.info('Bower Installing:', project.name); 21 | 22 | // TODO: remove sync methods 23 | fs.writeFileSync(project.tempPath + '/bower.json', bowerJson); 24 | if (project.bower.rc) { 25 | fs.writeFileSync(project.tempPath + '/.bowerrc', JSON.stringify(project.bower.rc)); 26 | } 27 | 28 | var cmd = path.join(__dirname, '..', '..', 'node_modules', '.bin', 'bower'); 29 | var args = ['install', '--config.interactive=false']; 30 | if (options.production) { 31 | args.push('--production'); 32 | } 33 | var cli = spawn(cmd, args, { cwd: project.tempPath }); 34 | 35 | cli.stdout.on('data', function (data) { 36 | if (data && data.length > 0) { 37 | log.debug(data.toString().trim()); 38 | } 39 | }); 40 | 41 | cli.stderr.on('data', function (data) { 42 | if (data && data.length > 0) { 43 | errorLog += data.toString().trim(); 44 | log.debug(data.toString().trim()); 45 | } 46 | }); 47 | 48 | cli.on('close', function (code) { 49 | cli.kill('SIGINT'); 50 | if (code === 0) { 51 | log.info('Bower Install complete:', project.name); 52 | fs.unlink(project.tempPath + '/bower.json', function () { 53 | fs.unlink(project.tempPath + '/.bowerrc', function () { 54 | return resolve(); 55 | }); 56 | }); 57 | } else { 58 | return reject(errorLog); 59 | } 60 | 61 | }); 62 | }); 63 | 64 | 65 | }; 66 | 67 | return { 68 | install: install 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /lib/installers/npm.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var spawn = require('cross-spawn-async'); 4 | var Promise = require('bluebird'); 5 | 6 | module.exports = function (log) { 7 | 8 | var install = function (project, options) { 9 | options = options || {}; 10 | return new Promise(function(resolve, reject) { 11 | 12 | var npmJson = JSON.stringify({ 13 | dependencies: project.npm.dependencies, 14 | devDependencies: options.production ? {} : project.npm.devDependencies, 15 | name: project.name 16 | }, null, 4); 17 | 18 | log.info('NPM Installing:', project.name); 19 | 20 | fs.writeFileSync(path.join(project.tempPath, 'package.json'), npmJson); 21 | 22 | if (project.npm.shrinkwrap) { 23 | var shrinkwrap = JSON.stringify(project.npm.shrinkwrap); 24 | fs.writeFileSync(path.join(project.tempPath, 'npm-shrinkwrap.json'), shrinkwrap); 25 | } 26 | 27 | var cmd = path.join(__dirname, '..', '..', 'node_modules', '.bin', 'npm'); 28 | var args = ['install', '--ignore-scripts']; 29 | if (options.production) { 30 | args.push('--production'); 31 | } 32 | 33 | var cli = null; 34 | var errorLog = ''; 35 | 36 | try { 37 | cli = spawn(cmd, args, { cwd: project.tempPath }); 38 | } catch (e) { 39 | log.error('NPM Exception', e, 'with command', cmd); 40 | } 41 | 42 | cli.stdout.on('data', function (data) { 43 | if (data && data.length > 0) { 44 | log.debug(data.toString().trim()); 45 | } 46 | }); 47 | 48 | cli.stderr.on('data', function (data) { 49 | if (data && data.length > 0) { 50 | errorLog += data.toString().trim(); 51 | log.debug(data.toString().trim()); 52 | } 53 | }); 54 | 55 | cli.on('close', function (code) { 56 | cli.kill('SIGINT'); 57 | log.info('NPM Install Complete:', project.name, 'Code:', code); 58 | if (code === 0) { 59 | fs.unlink(path.join(project.tempPath, 'package.json'), function () { 60 | fs.unlink(path.join(project.tempPath, 'npm-shrinkwrap.json'), function () { 61 | return resolve(); 62 | }); 63 | }); 64 | } else { 65 | fs.unlink(path.join(project.tempPath, 'npm-debug.log'), function () { 66 | return reject(errorLog); 67 | }); 68 | } 69 | 70 | }); 71 | 72 | }); 73 | }; 74 | 75 | return { 76 | install: install 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default Configuration. 3 | * See https://github.com/mozilla/node-convict/blob/master/README.md for details. 4 | */ 5 | var convict = require('convict'); 6 | 7 | module.exports = function () { 8 | 9 | // Check if we need to auto configure for a fast start. 10 | var env = process.env.NODE_ENV || 'dev'; 11 | var configFile = process.env.FREIGHT_CONFIG || __dirname + '/' + env + '.json'; 12 | 13 | require('./autoconfig')(configFile); 14 | 15 | var conf = convict({ 16 | env: { 17 | doc: 'The applicaton environment.', 18 | format: [ 'dev', 'test', 'stage', 'prod', 'production' ], 19 | default: 'dev', 20 | env: 'NODE_ENV' 21 | }, 22 | log: { 23 | level: { 24 | default: 'info', 25 | env: 'LOG_LEVEL' 26 | } 27 | }, 28 | ip: { 29 | doc: 'The IP address to bind.', 30 | format: 'ipaddress', 31 | default: '127.0.0.1', 32 | env: 'IP_ADDRESS' 33 | }, 34 | port: { 35 | doc: 'The port to bind.', 36 | format: 'port', 37 | default: 8872, 38 | env: 'PORT' 39 | }, 40 | limit: { 41 | doc: 'The bundle transmission size limit, in kb.', 42 | format: 'nat', 43 | default: 500 44 | }, 45 | password: { 46 | doc: 'The password that is used to create Freight bundles.', 47 | format: String, 48 | default: '' 49 | }, 50 | storage: { 51 | // TODO: You need to create this directory if it does not exist. 52 | // This directory is also used as a static file directory for Freight bundles. 53 | doc: 'Default bundle storage directory. Make sure it is somewhere in the Freight Server directory.', 54 | format: String, 55 | default: __dirname + '/../storage' 56 | }, 57 | tempDir: { 58 | // TODO: You need to create this directory if it does not exist. 59 | doc: 'Default directory for temporary files.', 60 | format: String, 61 | default: __dirname + '/../temp' 62 | }, 63 | // Redis config, see https://github.com/learnboost/kue#redis-connection-settings 64 | redis: { 65 | port: { 66 | doc: 'Redis Port', 67 | format: 'port', 68 | default: 6379 69 | }, 70 | host: { 71 | doc: 'Redis IP address to bind.', 72 | format: 'ipaddress', 73 | default: '127.0.0.1' 74 | }, 75 | auth: { 76 | doc: 'Redis Password.', 77 | format: String, 78 | default: '' 79 | }, 80 | options: { 81 | doc: 'Redis Options.', 82 | format: Object, 83 | default: {} 84 | } 85 | }, 86 | track: { 87 | delay: { 88 | doc: 'Repository update check delay in milliseconds', 89 | format: 'nat', 90 | default: 60 * 60000 91 | } 92 | } 93 | }); 94 | 95 | // load environment dependent configuration 96 | // TODO: development only for now, change it later. 97 | conf.loadFile(configFile); 98 | // perform configuration validation 99 | conf.validate(); 100 | 101 | return conf; 102 | }; 103 | -------------------------------------------------------------------------------- /lib/tracker.js: -------------------------------------------------------------------------------- 1 | var spawn = require('cross-spawn-async'); 2 | var path = require('path'); 3 | 4 | var Promise = require('bluebird'); 5 | var rimraf = require('rimraf'); 6 | var freight = require('freight')(); 7 | 8 | module.exports = function (log, conf, jobs) { 9 | function Tracker() { 10 | } 11 | 12 | jobs.promote(); 13 | 14 | jobs.process('check_repository', 1, function (job, done) { 15 | var repository = job.data.repository; 16 | var branch = job.data.branch; 17 | var extraOptions = job.data.extraOptions || {}; 18 | 19 | log.debug('Tracker: Fetching repository:', repository, 'Branch:', branch); 20 | return fetchRepository(repository, branch) 21 | .then(function (projectDirectory) { 22 | log.debug('Tracker: Got project directory', projectDirectory); 23 | var options = { 24 | // TODO: http(s) 25 | url: 'http://' + conf.get('ip') + ':' + conf.get('port') + '/', 26 | action: 'create', 27 | server: true, 28 | log: log, 29 | directory: projectDirectory, 30 | verbose: true 31 | }; 32 | 33 | if (extraOptions.trackDirectory) { 34 | options.directory = path.join(projectDirectory, extraOptions.trackDirectory); 35 | } 36 | 37 | return freight.init(options) 38 | .then( 39 | function () { 40 | log.debug('Tracker: Freight done.'); 41 | return new Promise(function (resolve, reject) { 42 | rimraf(projectDirectory, function (err) { 43 | if (err) { 44 | return reject(err); 45 | } 46 | log.debug('Tracker: Directory Clean:', projectDirectory); 47 | return resolve(); 48 | }); 49 | }); 50 | } 51 | ); 52 | }) 53 | .then( 54 | function() { 55 | log.debug('Tracker: Scheduling another check_repository.'); 56 | jobs.create('check_repository', { 57 | title: repository + '_' + branch, 58 | repository: repository, 59 | branch: branch, 60 | extraOptions: extraOptions 61 | }) 62 | .delay(conf.get('track').delay) 63 | .save(); 64 | 65 | done(); 66 | }, 67 | function (err) { 68 | log.error('Tracker failed.'); 69 | log.error(err); 70 | done(err); 71 | } 72 | ); 73 | 74 | }); 75 | 76 | Tracker.create = function (repository, branch, extraOptions, callback) { 77 | log.debug('Calling create'); 78 | var job = jobs.create('check_repository', { 79 | title: repository + '_' + branch, 80 | repository: repository, 81 | branch: branch, 82 | extraOptions: extraOptions 83 | }).save(); 84 | 85 | job.on('complete', function(){ 86 | log.info('Job completed'); 87 | callback(null); 88 | }).on('failed', function(){ 89 | callback(new Error('Create failed')); 90 | }) 91 | }; 92 | 93 | function fetchRepository(repository, branch) { 94 | return new Promise(function (resolve, reject) { 95 | var cloneDir = path.join(conf.get('tempDir'), 'clone' + Date.now()); 96 | var cmd = 'git'; 97 | var args = ['clone', repository, '-b', branch, '--depth=1', cloneDir]; 98 | var gitClone = spawn(cmd, args, { cwd: process.cwd() }); 99 | 100 | gitClone.stdout.on('data', function (data) { 101 | if (data) { 102 | log.debug(data.toString()); 103 | } 104 | }); 105 | 106 | gitClone.stderr.on('data', function (data) { 107 | if (data) { 108 | log.debug(data.toString()); 109 | } 110 | }); 111 | 112 | gitClone.on('close', function (code) { 113 | if (code === 0) { 114 | resolve(cloneDir); 115 | } else { 116 | reject(); 117 | } 118 | }); 119 | }); 120 | } 121 | 122 | return Tracker; 123 | }; 124 | -------------------------------------------------------------------------------- /routes/freight.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var kue = require('kue'); 5 | 6 | module.exports = function (log, conf) { 7 | 8 | var processor = require('../lib/job_processor')(log); 9 | log.debug('Redis Configuration', conf.get('redis')); 10 | var jobs = kue.createQueue({ 11 | redis: conf.get('redis') 12 | }); 13 | 14 | processor.setup(jobs); 15 | 16 | var freighter = require('../lib/freighter')(log, conf, jobs); 17 | var tracker = require('../lib/tracker')(log, conf, jobs); 18 | var freightAuth = require('../lib/auth')(log, conf); 19 | var FreightRoutes = {}; 20 | 21 | FreightRoutes.check = function (req, res) { 22 | if (! req.body && ! req.body.project && ! req.body.project.name) { 23 | return res.send(404); 24 | } 25 | 26 | var project = req.body.project; 27 | var extra = req.body.extra; 28 | // TODO: Switch to something else or keep md5? 29 | project.hash = crypto.createHash('md5').update(JSON.stringify(project)).digest('hex'); 30 | // storage directory for projects 31 | project.storageDir = conf.get('storage'); 32 | // path where tar.gz will be saved 33 | project.bundlePath = path.join(project.storageDir, project.name + '-' + project.hash + '.tar.gz'); 34 | project.productionBundlePath = path.join(project.storageDir, project.name + '-production-' + project.hash + '.tar.gz'); 35 | // temp storage directory where things install to 36 | project.tempPath = path.join(project.storageDir, project.hash); 37 | 38 | log.debug('Incoming Project', project, extra); 39 | 40 | // check if Freight file exists 41 | fs.exists(project.bundlePath, function (bundleExists) { 42 | var response = { 43 | creating: false, 44 | available: false, 45 | authenticated: freightAuth.checkPassword(extra.password) 46 | }; 47 | 48 | if (bundleExists) { 49 | response.available = true; 50 | response.hash = project.hash; 51 | } 52 | 53 | if (freightAuth.checkPassword(extra.password) && extra.create === 'true') { 54 | // TODO: delete stale jobs, try again to cache, fail if tries too many times. 55 | // TODO: job in progress with the same hash should stop this one. 56 | // TODO: restart stale job if timeout > x. 57 | if (! bundleExists || extra.force === 'true') { 58 | response.creating = true; 59 | freighter.create(project, extra); 60 | } 61 | } 62 | 63 | return res.json(response); 64 | 65 | }); 66 | }; 67 | 68 | FreightRoutes.download = function (req, res) { 69 | log.debug('Download request', req.body); 70 | if (req.body.hash) { 71 | var hashFile = path.join(conf.get('storage'), req.body.name + '-' + req.body.hash + '.tar.gz'); 72 | if (req.body.options && req.body.options.production === 'true') { 73 | hashFile = path.join(conf.get('storage'), req.body.name + '-production-' + req.body.hash + '.tar.gz'); 74 | } 75 | 76 | fs.exists(hashFile, function (exists) { 77 | if (exists) { 78 | log.debug('Download bundle:', hashFile); 79 | return res.sendfile(hashFile); 80 | } else { 81 | log.debug('Bundle does not exist:', hashFile); 82 | return res.send(404); 83 | } 84 | }); 85 | } else { 86 | log.debug('Hash not set.'); 87 | return res.send(404); 88 | } 89 | 90 | }; 91 | 92 | FreightRoutes.track = function (req, res) { 93 | if (req.body && req.body.repository && req.body.password && req.body.branch) { 94 | log.debug('Tracking request:', req.body); 95 | 96 | if (! freightAuth.checkPassword(req.body.password)) { 97 | log.debug('Password does not match'); 98 | return res.send(403); 99 | } 100 | 101 | var extraOptions = { 102 | trackDirectory: req.body.trackDirectory 103 | }; 104 | 105 | tracker.create(req.body.repository, req.body.branch, extraOptions, function (err) { 106 | if (err) { 107 | // fetch $REPO, run freight on it 108 | // keep fetching the master branch, run freight on it 109 | log.debug('Cannot track repository: ', err); 110 | return res.send(500); 111 | } else { 112 | return res.send(200); 113 | } 114 | }); 115 | 116 | 117 | } else { 118 | log.debug('Repository or password not set'); 119 | return res.send(500); 120 | } 121 | 122 | }; 123 | 124 | return FreightRoutes; 125 | }; 126 | -------------------------------------------------------------------------------- /lib/job_processor.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var rimraf = require('rimraf'); 3 | var archiver = require('archiver'); 4 | var Promise = require('bluebird'); 5 | var Job = require('kue').Job; 6 | 7 | module.exports = function (log) { 8 | 9 | var bower = require('./installers/bower')(log); 10 | var npm = require('./installers/npm')(log); 11 | 12 | function JobProcessor() { 13 | } 14 | 15 | JobProcessor.setup = function (jobs) { 16 | 17 | // TODO: one job at a time, npm can explode? 18 | // TODO: can Bower and NPM installa the same time? 19 | // TODO: remove jobs when errored. 20 | jobs.process('install', 1, function (job, done) { 21 | var steps = 7; 22 | var project = job.data.project; 23 | log.debug('Project Data:', project); 24 | log.debug('Creating Production Bundle'); 25 | job.progress(0, steps); 26 | return Promise.resolve() 27 | // Create Production bundle 28 | .then(function () { 29 | job.progress(1, steps); 30 | if (project.npm) { 31 | job.log('Running NPM Install Production'); 32 | return npm.install(project, { production: true }); 33 | } else { 34 | return Promise.resolve(); 35 | } 36 | }) 37 | .then(function () { 38 | job.progress(2, steps); 39 | if (project.bower) { 40 | job.log('Running Bower Install Production'); 41 | return bower.install(project, { production: true }) 42 | } else { 43 | return Promise.resolve(); 44 | } 45 | }) 46 | .then(function () { 47 | job.progress(3, steps); 48 | return compressProject(project, project.productionBundlePath) 49 | }) 50 | // Create Development bundle 51 | .then(function () { 52 | job.progress(4, steps); 53 | log.debug('Creating Development Bundle'); 54 | if (project.npm) { 55 | return npm.install(project); 56 | } else { 57 | return Promise.resolve(); 58 | } 59 | }) 60 | .then(function () { 61 | job.progress(5, steps); 62 | if (project.bower) { 63 | return bower.install(project); 64 | } else { 65 | return Promise.resolve(); 66 | } 67 | }) 68 | .then(function () { 69 | job.progress(6, steps); 70 | return compressProject(project, project.bundlePath); 71 | }) 72 | .then(function () { 73 | job.progress(7, steps); 74 | return cleanUp(project); 75 | }) 76 | .then( 77 | function () { 78 | done(); 79 | }, 80 | function (err) { 81 | done(err); 82 | } 83 | ); 84 | 85 | }); 86 | 87 | // remove stale jobs 88 | jobs.on('job complete', function (id) { 89 | Job.get(id, function (err, job) { 90 | if (err) { 91 | log.error(err); 92 | return; 93 | } 94 | job.remove(function (err) { 95 | if (err) { 96 | log.error(err); 97 | throw err; 98 | } 99 | log.info('removed completed job #%d', job.id); 100 | }); 101 | }); 102 | }); 103 | }; 104 | 105 | function compressProject(project, path) { 106 | return new Promise(function (resolve, reject) { 107 | log.info('Compressing project:', project.name); 108 | var start = Date.now(); 109 | 110 | var progressFilepath = path.replace('.tar.gz', '_inprogress.tar.gz'); 111 | var output = fs.createWriteStream(progressFilepath); 112 | var archive = archiver('tar', { 113 | gzip: true, 114 | gzipOptions: { 115 | level: 6 116 | } 117 | }); 118 | 119 | output.on('close', function () { 120 | fs.rename(progressFilepath, path); 121 | var end = Date.now(); 122 | log.info('Bundle created:', path); 123 | log.info('Compression completed in ', (end - start) / 1000, 'seconds.', archive.pointer() + ' total bytes'); 124 | 125 | resolve(); 126 | }); 127 | 128 | archive.on('error', function (err) { 129 | log.info('Archiver error', err); 130 | throw err; 131 | }); 132 | 133 | archive.pipe(output); 134 | archive.bulk([ 135 | { expand: true, cwd: project.tempPath, src: ['**'], dot: true } 136 | ]); 137 | archive.finalize(); 138 | }); 139 | } 140 | 141 | function cleanUp(project) { 142 | return new Promise(function (resolve, reject) { 143 | // TODO: need an option to keep the tempPath sometimes. 144 | if (true && project.tempPath && project.tempPath.length > 0) { 145 | rimraf(project.tempPath, function () { 146 | log.debug('Directory Clean:', project.tempPath); 147 | resolve(); 148 | }); 149 | } else { 150 | resolve(); 151 | } 152 | }); 153 | } 154 | 155 | return JobProcessor; 156 | }; 157 | -------------------------------------------------------------------------------- /static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.1.1 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.isLoading=!1};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",f.resetText||d.data("resetText",d[e]()),d[e](f[b]||this.options[b]),setTimeout(a.proxy(function(){"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},b.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}a&&this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});return this.$element.trigger(j),j.isDefaultPrevented()?void 0:(this.sliding=!0,f&&this.pause(),this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid.bs.carousel")},0)}).emulateTransitionEnd(1e3*d.css("transition-duration").slice(0,-1))):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid.bs.carousel")),f&&this.cycle(),this)};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("collapse in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?void this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);!e&&f.toggle&&"show"==c&&(c=!c),e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(b){a(d).remove(),a(e).each(function(){var d=c(a(this)),e={relatedTarget:this};d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown",e)),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown",e))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('
').insertAfter(a(this)).on("click",b);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;f.toggleClass("open").trigger("shown.bs.dropdown",h),e.focus()}return!1}},f.prototype.keydown=function(b){if(/(38|40|27)/.test(b.keyCode)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var f=c(d),g=f.hasClass("open");if(!g||g&&27==b.keyCode)return 27==b.which&&f.find(e).focus(),d.click();var h=" li:not(.divider):visible a",i=f.find("[role=menu]"+h+", [role=listbox]"+h);if(i.length){var j=i.index(i.filter(":focus"));38==b.keyCode&&j>0&&j--,40==b.keyCode&&j| t |