Files
cloudron-box/server.js
T
2013-09-26 10:18:53 -07:00

269 lines
8.3 KiB
JavaScript
Executable File

#!/usr/bin/env node
'use strict';
var optimist = require('optimist'),
express = require('express'),
http = require('http'),
HttpError = require('./api/httperror'),
path = require('path'),
fs = require('fs'),
mkdirp = require('mkdirp'),
db = require('./api/database.js'),
routes = require('./api/routes'),
debug = require('debug')('server.js'),
crypto = require('crypto'),
os = require('os'),
polo = require('polo'),
assert = require('assert'),
pkg = require('./package.json');
// some configs, should maybe go into a config file? - Johannes
var REQUEST_LIMIT='10mb';
exports = module.exports = {
start: start,
stop: stop,
VERSION: pkg.version
};
function getUserHomeDir() {
return process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
}
var baseDir = path.join(getUserHomeDir(), '.yellowtent');
var argv = optimist.usage('Usage: $0 --dataRoot <directory>')
.alias('h', 'help')
.describe('h', 'Show this help.')
.alias('d', 'dataRoot')
.default('d', path.join(baseDir, 'data'))
.describe('d', 'Volume data storage directory.')
.string('d')
.alias('m', 'mountRoot')
.default('m', path.join(baseDir, 'mount'))
.describe('m', 'Volume mount point directory.')
.string('m')
.alias('s', 'silent')
.default('s', false)
.describe('s', 'Suppress console output for non errors.')
.boolean('s')
.alias('c', 'configRoot')
.default('c', path.join(baseDir, 'config'))
.describe('c', 'Server config root directory for storing user db and meta data.')
.string('c')
.alias('p', 'port')
.describe('p', 'Server port')
.argv;
// print help and die if requested
if (argv.h) {
optimist.showHelp();
process.exit(0);
}
// Error handlers. These are called until one of them sends headers
function clientErrorHandler(err, req, res, next) {
var status = err.status || err.statusCode; // connect/express or our app
if (status >= 400 && status <= 499) {
res.send(status, JSON.stringify({ status: http.STATUS_CODES[status], message: err.message }));
debug(http.STATUS_CODES[status] + ' : ' + err.message);
debug(err.stack);
} else {
next(err);
}
}
function serverErrorHandler(err, req, res, next) {
var status = err.status || err.statusCode || 500;
res.send(status, http.STATUS_CODES[status] + ' : ' + err.message);
console.error(http.STATUS_CODES[status] + ' : ' + err.message);
console.error(err.stack);
}
function getVersion(req, res, next) {
if (req.method !== 'GET') return next(new HttpError(405, 'Only GET supported'));
res.send({ version: exports.VERSION });
}
function initialize(config, callback) {
var app = express();
app.configure(function () {
var json = express.json({ strict: true, limit: REQUEST_LIMIT }), // application/json
urlencoded = express.urlencoded({ limit: REQUEST_LIMIT }); // application/x-www-form-urlencoded
if (!config.silent) {
app.use(express.logger({ format: 'dev', immediate: false }));
}
app.use(express.timeout(10000))
.use('/', express.static(__dirname + '/webadmin')) // use '/' for now so cookie is not restricted to '/webadmin'
.use(json)
.use(urlencoded)
.use(express.cookieParser())
.use(express.favicon(__dirname + "/assets/favicon.ico"))
// API calls that do not require authorization
.use('/api/v1/version', getVersion)
.use('/api/v1/firsttime', routes.user.firstTime)
.use('/api/v1/createadmin', routes.user.createAdmin) // ## FIXME: allow this before auth for now
.use(routes.user.authenticate)
.use(app.router)
.use(clientErrorHandler)
.use(serverErrorHandler);
// routes controlled by app.router
app.post('/api/v1/token', routes.user.createToken);
app.get('/api/v1/logout', routes.user.logout);
app.post('/api/v1/user/create', routes.user.create);
app.post('/api/v1/user/remove', routes.user.remove);
app.get('/api/v1/user/info', routes.user.info);
app.param('volume', routes.volume.attachVolume);
app.post('/api/v1/sync/:volume/diff', routes.sync.diff);
app.post('/api/v1/sync/:volume/delta', routes.sync.delta);
app.get('/api/v1/revisions/:volume/*', routes.file.revisions);
app.get('/api/v1/file/:volume/*', routes.file.read);
app.get('/api/v1/metadata/:volume/*', routes.file.metadata);
app.post('/api/v1/file/:volume/*', routes.file.multipart, routes.file.update);
app.put('/api/v1/file/:volume/*', routes.file.multipart, routes.file.putFile);
app.post('/api/v1/fileops/:volume/copy', express.json({ strict: true }), routes.fileops.copy);
app.post('/api/v1/fileops/:volume/move', express.json({ strict: true }), routes.fileops.move);
app.post('/api/v1/fileops/:volume/delete', express.json({ strict: true }), routes.fileops.remove);
app.del('/api/v1/fileops/:volume/*', routes.fileops.remove);
app.get('/api/v1/volume/:volume/list/', routes.volume.listFiles);
app.get('/api/v1/volume/:volume/list/*', routes.volume.listFiles);
app.get('/api/v1/volume/list', routes.volume.listVolumes);
app.post('/api/v1/volume/create', routes.volume.createVolume);
app.post('/api/v1/volume/:volume/delete', routes.volume.deleteVolume);
app.post('/api/v1/volume/:volume/mount', routes.volume.mount);
app.post('/api/v1/volume/:volume/unmount', routes.volume.unmount);
});
app.set('port', config.port);
if (!config.silent) {
console.log('Server listening on port ' + app.get('port'));
console.log('Using data root:', config.dataRoot);
console.log('Using config root:', config.configRoot);
console.log('Using mount root:', config.mountRoot);
}
// ensure data/config/mount paths
mkdirp.sync(config.dataRoot);
mkdirp.sync(config.configRoot);
mkdirp.sync(config.mountRoot);
if (!db.initialize(config)) {
return callback(new Error('Error initializing database'));
}
routes.sync.initialize(config);
routes.volume.initialize(config);
callback(null, app);
}
function listen(app, callback) {
app.httpServer = http.createServer(app);
function callbackWrapper(error) {
if (callback) {
callback(error);
callback = undefined;
} else {
console.error('Try to call back twice', error);
}
}
app.httpServer.listen(app.get('port'), function (err) {
if (err) return callbackWrapper(err);
callbackWrapper();
});
app.httpServer.on('error', function (err) {
callbackWrapper(err);
});
}
function announce(app, callback) {
var services = polo();
services.put({
name: 'yellowtent',
port: app.get('port')
});
services.on('error', function (error) {
console.error('Unable to announce the device.', error);
});
callback();
}
function start(config, callback) {
assert(typeof config === 'object');
assert(typeof callback === 'function');
initialize(config, function (err, app) {
if (err) return callback(err);
listen(app, function (err) {
if (err) return callback(err);
announce(app, function (err) {
if (err) return callback(err);
callback(null, app);
});
});
});
}
function stop(app, callback) {
// Any other way to check if app is an object we expect?
assert(app && app.httpServer);
assert(typeof callback === 'function');
if (!app.httpServer) {
return callback();
}
app.httpServer.close(function () {
app.httpServer.unref();
// TODO should delete the app variable
app = undefined;
callback();
});
}
// main entry point when running standalone
// TODO Maybe this should go into a new 'executeable' file - Johannes
if (require.main === module) {
var config = {
port: argv.p || 3000,
dataRoot: path.resolve(argv.d),
configRoot: path.resolve(argv.c),
mountRoot: path.resolve(argv.m),
silent: argv.s
};
start(config, function (err) {
if (err) {
console.error('Error starting server', err);
process.exit(1);
}
});
}