UNPKG

ares-ide

Version:

A browser-based code editor and UI designer for Enyo 2 projects

441 lines (395 loc) 13.5 kB
/* jshint node:true */ /** * Base toolkit for Hermes FileSystem providers implemented using Node.js */ var fs = require("graceful-fs"), path = require("path"), util = require("util"), express = require("express"), tunnel = require("tunnel"), log = require('npmlog'), HttpError = require("./httpError"), ServiceBase = require("./svcBase"); module.exports = FsBase; /** * Base object for Ares file-system services * * @param {Object} config * @property config {String} port requested IP port (0 for dynamic allocation, the default) * @property config {String} pathname location after the service origin, defaults to '/' * @property config {String} basename child class name (for tracing) * @property config {String} level tracing level (default to 'http') * @property config {Boolean} performCleanup clean temporary files & folders (default to true) * * @param {Function} next * @param next {Error} err * @param next {Object} service * @property service {String} protocol is 'http' or 'https' * @property service {String} host IP address to * @property service {String} port bound port (useful in case of dynamic allocation) * @property service {String} origin consolidated string of protocol, host & port * @property service {String} pathname to locat the service behind the origin * * @public */ function FsBase(config, next) { ServiceBase.call(this, config, next); // sanity check [ // middleware methods (always executed) 'allowLocalOnly', 'respond', // admin methods 'getUserInfo', 'setUserInfo', // filesystem verbs 'propfind', 'get', 'put', 'mkcol', 'delete', 'move', 'copy' ].forEach((function(method) { if ((typeof(this[method]) !== 'function') || (this[method].length !== 3)) { setImmediate(next, new Error("BUG: method '" + method + "' is not a 3-parameters function")); return; } }).bind(this)); } util.inherits(FsBase, ServiceBase); /** * Additionnal middlewares: 'this.app.use(xxx)' * @protected */ FsBase.prototype.use = function() { log.verbose('FsBase#use()'); this.app.use(express.cookieParser()); this.app.use(this.config.pathname, this.authorize.bind(this)); this.app.use(express.methodOverride()); this.app.use(this.dump.bind(this)); }; FsBase.prototype.cleanProcess = function(next) { log.verbose('FsBase#cleanProcess()'); setImmediate(next); }; /** * Additionnal routes/verbs: 'this.app.get()', 'this.app.port()' * @protected */ FsBase.prototype.route = function() { log.verbose('FsBase#route()'); // URL-scheme: '/' to get/set user credentials this.route0 = this.makeExpressRoute.bind(this)(''); log.verbose("FsBase#route()", "GET/POST:", this.route0); this.app.get(this.route0, this.getUserInfo.bind(this)); this.app.post(this.route0, this.setUserInfo.bind(this)); // 3. Handle HTTP verbs // URL-scheme: ID-based file/folder tree navigation, used by // HermesClient. this.route1 = this.makeExpressRoute.bind(this)('/id/'); this.route2 = this.makeExpressRoute.bind(this)('/id/:id'); log.verbose("FsBase#route()", "ALL:", this.route1); this.app.all(this.route1, (function(req, res, next) { req.params.id = this.encodeFileId('/'); req.params.path = '/'; _handle.bind(this)(req, res, next); }).bind(this)); log.verbose("FsBase#route()", "ALL:", this.route2); this.app.all(this.route2, [_parseIdUrl.bind(this)], _handle.bind(this)); function _parseIdUrl(req, res, next) { log.silly("FsBase#_parseIdUrl()", "parsing file id:", req.params.id); req.params.id = req.params.id || this.encodeFileId('/'); req.params.path = this.decodeFileId(req.params.id); setImmediate(next); } // URL-scheme: WebDAV-like navigation, used by the Enyo loader // (itself used by the Enyo Javacript parser to analyze the // project source code) & by the Ares project preview. function _parseFileUrl(req, res, next) { log.silly("FsBase#_parseFileUrl()", "parsing file path:", req.params[0]); req.params.path = req.params[0]; req.params.id = this.encodeFileId(req.params.path); setImmediate(next); } var overlays = { // "$deimos/source/designer/designerFrame/*" designer: path.join(__dirname, "..", "..", "deimos", "source", "designer", "designerFrame"), // "node_modules/less/dist/*" less: path.join(__dirname, "..", "..", "node_modules", "less", "dist") }; function _parseOverlays(req, res, next) { var overlayDir = overlays[req.query.overlay]; if (overlayDir) { log.silly("FsBase#_parseOverlays()", "checking for overlay='" + req.query.overlay + "' files..."); // We cannot use express.static(), because it would serve // designer files always from the same mount-point in // the '/file' tree. var filePath = path.join(overlayDir, path.basename(req.params.path)); fs.stat(filePath, (function(err, stats) { if (err) { next(err); } else if (stats.isFile()) { log.silly("FsBase#_parseOverlays()", "found overlay file:", filePath); res.status(200); res.sendfile(filePath); } else { next(); } }).bind(this)); } else { setImmediate(next); } } this.route3 = this.makeExpressRoute.bind(this)('/file/*'); log.verbose("FsBase#route()", "ALL:", this.route3); this.app.all(this.route3, [_parseFileUrl.bind(this), _parseOverlays.bind(this)], _handle.bind(this)); function _handle(req, res, next) { var method = req.method.toLowerCase(); log.verbose("FsBase#_handle()", "method:", method, "req.params.id:", req.params.id, "req.params.path:", req.params.path); this[method](req, res, this.respond.bind(this, res)); } log.verbose("FsBase#route()", "app.routes:", this.app.routes); }; /** * Global error handler (arity === 4) * @protected */ FsBase.prototype.errorHandler = function(err, req, res, next){ log.error("FsBase#errorHandler", err.stack); this.respond(res, err); }; // Middlewares -- one per session // Authorize FsBase.prototype.authorize = function(req, res, next) { log.verbose("FsBase#authorize()", "checking that request comes from 127.0.0.1"); if (req.connection.remoteAddress !== "127.0.0.1") { setImmediate(next, new HttpError("Access denied from IP address "+req.connection.remoteAddress, 401 /*Unauthorized*/)); } else { log.verbose("FsBase#authorize()", "Ok"); setImmediate(next); } }; /** * Normalize a path using only `/`, to make it usable in URL's * @param {String} p the path to normalize */ if (process.platform === 'win32') { FsBase.prototype.normalize = function(p) { return path.normalize(p).replace(/\\/g,'/'); }; } else { FsBase.prototype.normalize = function(p) { return path.normalize(p); }; } /** * Turns an {Error} object into a usable response {Object} * * A response {Object} as #code and #body properties. This method is * expecyted to be overriden by sub-classes of {FsBase}. * * @param {Error} err the error object to convert. */ FsBase.prototype.errorResponse = function(err) { log.warn("FsBase#errorResponse()", "err:", err); var response = { code: 403, // Forbidden body: err.toString() }; if (err instanceof Error) { response.code = err.statusCode || 403 /*Forbidden*/; response.body = err.toString(); log.warn("FsBase#errorResponse()", err.stack); } log.info("FsBase#errorResponse()", "response:", response); return response; }; /** * Unified response handler * @param {Object} res the express response {Object} * @param {Object} err the error if any. Can be any kind of {Error}, such as an { HttpError} * @param {Object} response is a response {Object} * * A response {Object} has: * * - Two mandatory properties: #code (used as the HTTP statusCode) and * #body (inlined in the response body, of not falsy). * - One optional #headers property is an {Object} of HTTP headers to * be carried into the response message */ FsBase.prototype.respond = function(res, err, response) { log.verbose("FsBase#respond()", "response:", response); if (err) { response = this.errorResponse(err); } if (response && response.headers) { for (var h in Object.keys(response.headers)) { res.setHeader(h, response.headers); } } if (response && response.body) { res.status(response.code).send(response.body); } else if (response) { res.status(response.code).end(); } else { log.silly("FsBase#respond()", "response already sent or being streamed"); } }; FsBase.prototype.encodeFileId = function(filePath) { log.silly("FsBase#encodeFileId()", "filePath:", filePath); var buf = new Buffer(filePath, 'utf-8'); var fileId = buf.toString('hex'); return fileId; }; FsBase.prototype.decodeFileId = function(fileId) { log.silly("FsBase#encodeFileId()", "fileId:", fileId); var buf = new Buffer(fileId, 'hex'); var filePath = buf.toString('utf-8'); return filePath; }; FsBase.prototype.parseProxy = function(config) { this.httpAgent = _makeAgent('http', config); this.httpsAgent = _makeAgent('https', config); function _makeAgent(protocol, config) { var proxyConfig = config.proxy && config.proxy[protocol]; if (!proxyConfig) { return undefined; } var tunnelConstructor = tunnel[protocol + proxyConfig.tunnel]; var agent; if (proxyConfig && typeof tunnelConstructor == 'function') { agent = tunnelConstructor({proxy: proxyConfig}); log.info("FsBase.parseProxy()", "protocol:", protocol, "agent:", agent); } else { log.warn("FsBase#parseProxy()", "protocol:", protocol, "invalid proxy configuration:", config.proxy, "will use default agent"); } return agent; } }; // Actions FsBase.prototype.dump = function(req, res, next) { //log.silly("FsBase.dump()", "req.keys=", Object.keys(req)); log.silly("FsBase#dump()", "req.method=", req.method); log.silly("FsBase#dump()", "req.headers=", req.headers); log.silly("FsBase#dump()", "req.url=", req.url); log.silly("FsBase#dump()", "req.query=", req.query); log.silly("FsBase#dump()", "req.cookies=", req.cookies); log.silly("FsBase#dump()", "req.body=", req.body); setImmediate(next); }; FsBase.prototype.getUserInfo = function(req, res, next) { log.verbose("FsBase#getUserInfo():"); setImmediate(next, null, { code: 200 /*Ok*/, body: {} }); }; FsBase.prototype.setUserInfo = function(req, res, next) { log.verbose("FsBase#setUserInfo():"); setImmediate(next, null, { code: 200 /*Ok*/, body: {} }); }; FsBase.prototype.put = function(req, res, next) { log.verbose("FsBase#put()"); if (req.is('application/x-www-form-urlencoded')) { // carry a single file at most return this._putWebForm(req, res, next); } else if (req.is('multipart/form-data')) { // can carry several files return this._putMultipart(req, res, next); } else { setImmediate(next, new Error("Unhandled upload of content-type='" + req.headers['content-type'] + "'")); } }; /** * Stores one file provided by a web form * * @param {HTTPRequest} req * @param {HTTPResponse} res * @param {Function} next(err, data) CommonJS callback */ FsBase.prototype._putWebForm = function(req, res, next) { // Mutually-agreed encoding of file name & location: // 'path' and 'name' var relPath, fileId, self = this, pathParam = req.param('path'), nameParam = req.param('name'); log.verbose("FsBase#putWebForm()", "pathParam:", pathParam, "nameParam:", nameParam); if (!pathParam) { setImmediate(next, new HttpError("Missing 'path' request parameter", 400 /*Bad Request*/)); return; } if (nameParam === '.'|| !nameParam) { relPath = pathParam; } else { relPath = [pathParam, nameParam].join('/'); } // get the bits: base64-encoded binary in the 'content' field var buf; if (req.body.content) { buf = new Buffer(req.body.content, 'base64'); } else { log.verbose("FsBase#putWebForm()", "empty file"); buf = new Buffer(''); } var urlPath = self.normalize(relPath); log.verbose("FsBase#putWebForm()", "storing file as", urlPath); fileId = self.encodeFileId(urlPath); self.putFile(req, { name: relPath, buffer: buf }, function(err){ log.verbose("FsBase#putWebForm()", "err:", err); next(err, { code: 201, // Created body: [{ id: fileId, path: urlPath, name: path.basename(urlPath), isDir: false }] }); }); }; /** * Stores one or more files provided by a multipart form * * @param {HTTPRequest} req * @param {HTTPResponse} res * @param {Function} next(err, data) CommonJS callback * @see {ServiceBase._storeMultiPart} */ FsBase.prototype._putMultipart = function(req, res, next) { log.verbose("FsBase#_putMultipart()"); var self =this; var nodes = []; var pathParam = req.param('path'); this._storeMultipart(req, _putOne, _finish); function _putOne(file, next) { file.name = file.name ? [pathParam, file.name].join('/') : pathParam; self.putFile(req, file, function _done(err, node) { log.silly("FsBase#_putMultipart#_putOne#_done()", "err:", err, "node:", node); if (node) { nodes.push(node); } next(); }); } function _finish(err) { log.silly("FsBase#_putMultipart#_finish()", "nodes:", nodes); next(err, { code: 201, // Created body: nodes }); } }; /** * Write a file in the filesystem * * Invokes the CommonJs callback with the created {ares.Filesystem.Node}. * * @param {Object} req the express request context * @param {Object} file contains mandatory #name property, plus either * #buffer (a {Buffer}) or #path (a temporary absolute location). * @param {Function} next a Common-JS callback */ FsBase.prototype.putFile = function(req, file, next) { next (new HttpError("ENOSYS", 500)); };