UNPKG

ares-ide

Version:

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

609 lines (564 loc) 17.2 kB
/* jshint node:true */ /** * fsLocal.js -- Ares FileSystem (fs) provider, using local files. * * This FileSystemProvider is both the simplest possible one * and a working sample for other implementations. */ // nodejs version checking is done in parent process ide.js var fs = require("graceful-fs"), path = require("path"), util = require("util"), mkdirp = require("mkdirp"), async = require("async"), log = require('npmlog'), copyFile = require("./lib/copyFile"), FsBase = require("./lib/fsBase"), HttpError = require("./lib/httpError"); var basename = path.basename(__filename, '.js'); /** * Ares local file-system service * @see {FsBase} * @param {Object} config * @property config {String} root local file-system folder that serves as root * @public */ function FsLocal(config, next) { // Use absolute local path config.root = path.resolve(config.root); // inherits FsBase (step 1/2) FsBase.call(this, config, next); } // inherits FsBase (step 2/2) util.inherits(FsLocal, FsBase); FsLocal.prototype._statusCodes = { 'ENOENT': 404, // Not-Found 'EPERM' : 403, // Forbidden 'EEXIST': 409, // Conflict 'ETIMEDOUT': 408 // Request-Timed-Out }; FsLocal.prototype.errorResponse = function(err) { log.warn("FsLocal#errorResponse()", "err:", err); var response = { code: 403, // Forbidden body: err.toString() }; if (err instanceof Error) { response.code = err.statusCode || this._statusCodes[err.code] || this._statusCodes[err.errno] || 403; // Forbidden response.body = err.toString(); delete err.statusCode; log.warn("FsLocal#errorResponse()", err.stack); } log.verbose("FsLocal#errorResponse()", "response:", response); return response; }; FsLocal.prototype.propfind = function(req, res, next) { // 'infinity' is '-1', 'undefined' is '0' var depthStr = req.param('depth'); var depth = depthStr ? (depthStr === 'infinity' ? -1 : parseInt(depthStr, 10)) : 1; this._propfind(null, req.param('path'), depth, function(err, content){ log.verbose("FsLocal#propfind()", "content:", content); next(err, {code: 200 /*Ok*/, body: content}); }); }; FsLocal.prototype.move = function(req, res, next) { this._changeNode(req, res, fs.rename, next); }; FsLocal.prototype.copy = function(req, res, next) { this._changeNode(req, res, this._cpr, next); }; FsLocal.prototype.get = function(req, res, next) { this._getFile(req, res, next); }; FsLocal.prototype.mkcol = function(req, res, next) { var newPath, newId, newName, self = this, pathParam = req.param('path'), nameParam = req.param('name'), overwriteParam = req.param('overwrite') !== "false"; log.verbose("FsLocal#mkcol()", "pathParam:", pathParam); log.verbose("FsLocal#mkcol()", "nameParam:", nameParam); if (!nameParam) { setImmediate(next, new HttpError("missing 'name' query parameter", 400 /*Bad-Request*/)); return; } newPath = path.relative('.', path.join('.', pathParam, nameParam)); log.verbose("FsLocal#mkcol()", "newPath:", newPath); if (newPath[0] === '.') { setImmediate(next, new HttpError("Attempt to navigate beyond the root folder: '" + newPath + "'", 403 /*Forbidden*/)); return; } newPath = '/' + self.normalize(newPath); newName = path.basename(newPath); newId = this.encodeFileId(newPath); var absPath = path.join(this.config.root, newPath); async.series([ this._checkOverwrite.bind(this, absPath, overwriteParam), mkdirp.bind(null, absPath) ], function(err) { if (err) { next(err); } else { next(null, { code: 201, // Created body: { id: newId, path: newPath, name: newName, isDir: true } }); } }); }; FsLocal.prototype['delete'] = function(req, res, next) { var pathParam = req.param('path'), localPath = path.join(this.config.root, pathParam); if (localPath === this.config.root) { setImmediate(next, new HttpError("Not allowed to remove service root", 403 /*Forbidden*/)); } else { this._rmrf(path.join(this.config.root, pathParam), (function(err) { // return the new content of the parent folder this._propfind(err, path.dirname(pathParam), 1 /*depth*/, function(err, content) { next(err, { code: 200 /*Ok*/, body: content }); }); }).bind(this)); } }; FsLocal.prototype._propfind = function(err, relPath, depth, next) { if (err) { setImmediate(next, err); return; } var localPath = path.join(this.config.root, relPath), urlPath = this.normalize(relPath); if (path.basename(relPath).charAt(0) ===".") { // Skip hidden files & folders (using UNIX // convention: XXX do it for Windows too) setImmediate(next); return; } fs.stat(localPath, (function(err, stat) { if (err) { next(err); return; } // minimum common set of properties var node = { path: urlPath, name: path.basename(urlPath), id: this.encodeFileId(urlPath), isDir: stat.isDirectory() }; // Give the top-level node the name (NOT the path) of the mount-point if (node.name === '') { node.name = path.basename(this.config.root); } log.silly("FsLocal#_propfind()", "relPath=" + relPath + ", depth="+depth+", node="+util.inspect(node)); if (stat.isFile() || !depth) { next(null, node); return; } else if (node.isDir) { node.children = []; fs.readdir(localPath, (function(err, files) { if (err) { next(err); // XXX or skip this directory... return; } if (!files.length) { next(null, node); return; } //to skip the files which user doesn't have permission to read files = files.filter(function(name){ return fs.existsSync(path.join(localPath, name)); }); var count = files.length; files.forEach(function(name) { this._propfind(null, path.join(relPath, name), depth-1, function(err, subNode){ if (err) { next(err); return; } if (subNode) { node.children.push(subNode); } if (--count === 0) { // return to upper layer only if // every nodes of this layer // were successfully parsed next(null, node); } }); }, this); }).bind(this)); } else { // skip special files setImmediate(next); } }).bind(this)); }; FsLocal.prototype._getFile = function(req, res, next) { var self = this; var relPath = req.param('path'); var localPath = path.join(this.config.root, relPath); log.verbose("FsLocal#_getFile()", "sending localPath=" + localPath); fs.stat(localPath, function(err, stat) { if (err) { setImmediate(next, err); return; } if (stat.isFile()) { self._propfind(err, relPath, 0 /*depth*/, function(err, node) { res.setHeader('x-ares-node', JSON.stringify(node)); res.status(200); res.sendfile(localPath); // return nothing: streaming response // is already in progress. setImmediate(next); }); } else if (stat.isDirectory() && req.param('format') === 'base64') { // Return the folder content as a FormData filled with base64 encoded file content var depthStr = req.param('depth'); var depth = depthStr ? (depthStr === 'infinity' ? -1 : parseInt(depthStr, 10)) : 1; log.verbose("FsLocal#_getFile()", "Preparing dir in base64, depth: " + depth + " " + localPath); self._propfind(null, req.param('path'), depth, function(err, content){ var parts = []; function addParts(entries) { entries.forEach(function(entry) { if (entry.isDir) { addParts(entry.children); } else { var part = { name: entry.path.substr(content.path.length + 1), path: path.join(localPath, entry.path.substr(content.path.length)) }; log.silly("FsLocal#_getFile()", "adding part: ", part); parts.push(part); } }); } addParts(content.children); self.returnFormData(parts, res, next); }); } else { next(new Error("not a file: '" + localPath + "'")); } }); }; // XXX ENYO-1086: refactor tree walk-down FsLocal.prototype._rmrf = function(localPath, next) { // from <https://gist.github.com/1526919> fs.stat(localPath, (function(err, stats) { if (err) { next(err); return; } if (!stats.isDirectory()) { return fs.unlink(localPath, next); } var count = 0; fs.readdir(localPath, (function(err, files) { if (err) { next(err); } else if (files.length < 1) { fs.rmdir(localPath, next); } else { files.forEach(function(file) { var sub = path.join(localPath, file); this._rmrf(sub, function(err) { if (err) { next(err); return; } if (++count == files.length) { return fs.rmdir(localPath, next); } }); }, this); } }).bind(this)); }).bind(this)); }; FsLocal.prototype.putFile = function(req, file, next) { var absPath = path.join(this.config.root, file.name), urlPath = this.normalize(file.name), dir = path.dirname(absPath), encodeFileId = this.encodeFileId, overwriteParam = req.param('overwrite') !== "false", node; log.verbose("FsLocal#putFile()", "file.name:", file.name, "-> absPath:", absPath); async.series([ this._checkOverwrite.bind(this, absPath, overwriteParam), mkdirp.bind(null, dir), function(next) { if (file.path) { log.verbose("FsLocal#putFile()", "moving/copying file"); try { fs.renameSync(file.path, absPath); setImmediate(next); } catch(err) { log.verbose("FsLocal#putFile()", "err:", err.toString()); if (err.code === 'EXDEV') { log.verbose("FsLocal#putFile()", "COPY+REMOVE file:", file.path, "-> absPath:", absPath); async.series([ copyFile.bind(undefined, file.path, absPath), fs.unlink.bind(fs, file.path) ], next); } else { throw err; } } } else if (file.buffer) { log.silly("FsLocal#putFile()", "writing buffer"); fs.writeFile(absPath, file.buffer, next); } else if (file.stream) { log.silly("FsLocal#putFile()", "writing stream"); var out = fs.createWriteStream(absPath); out.on('close', function() { log.silly("FsLocal#putFile()", "on-close, file.name:", file.name); next(); }); out.on('error', function(err) { log.silly("FsLocal#putFile()", "output file.name:", file.name, "on-err:", err); next(err); }); file.stream.on('error', function(err) { log.warn("FsLocal#putFile()", "input file.name:", file.name, "on-err:", err); next(err); }); file.stream.on('end', function() { log.silly("FsLocal#putFile()", "on-end: input"); }); file.stream.pipe(out, { end: true }); } else { setImmediate(next, new HttpError("cannot write file=" + JSON.stringify(file), 400)); } }, function(next){ log.verbose("FsLocal#putFile()", "wrote: file.name:", file.name); node = { id: encodeFileId(urlPath), path: urlPath, name: path.basename(urlPath), isDir: false }; setImmediate(next); } ], function(err) { next(err, node); }); }; FsLocal.prototype._checkOverwrite = function(absPath, overwrite, next) { if (!overwrite) { fs.stat(absPath, function(err, stat) { if (err) { if (err.code === 'ENOENT') { /* normal */ next(); } else { /* wrong */ next(new HttpError('Destination already exists', 412 /*Precondition-Failed*/)); } } else { /* wrong */ next(new HttpError('Destination already exists', 412 /*Precondition-Failed*/)); } }); } else { setImmediate(next); } }; // XXX ENYO-1086: refactor tree walk-down FsLocal.prototype._changeNode = function(req, res, op, next) { var pathParam = req.param('path'), nameParam = req.param('name'), folderIdParam = req.param('folderId'), overwriteParam = req.param('overwrite') !== "false", srcPath = path.join(this.config.root, pathParam); var dstPath, dstRelPath; var srcStat, dstStat; if (nameParam) { // rename/copy file within the same collection (folder) dstRelPath = path.join(path.dirname(pathParam), path.basename(nameParam)); } else if (folderIdParam) { // move/copy at a new location dstRelPath = path.join(this.decodeFileId(folderIdParam), path.basename(pathParam)); } else { setImmediate(next, new HttpError("missing query parameter: 'name' or 'folderId'", 400 /*Bad-Request*/)); return; } dstPath = path.join(this.config.root, dstRelPath); if (srcPath === dstPath) { setImmediate(next, new HttpError("trying to move a resource onto itself", 400 /*Bad-Request*/)); return; } async.waterfall([ fs.stat.bind(this, srcPath), function(stat, next) { srcStat = stat; fs.stat(dstPath, next); }, function(stat, next) { dstStat = stat; setImmediate(next); } ], function(err) { // see RFC4918, section 9.9.4 (MOVE Status // Codes) & section 9.8.5 (COPY Status Codes). if (err) { if (err.code === 'ENOENT') { if (err.path === srcPath) { /* srcPath doesn't exist */ setImmediate(next, new HttpError("resouce does not exist", 400 /*Bad-Request*/)); return; } else { /* dstPath doesn't exist */ // Destination resource does not exist yet op(srcPath, dstPath, (function(err) { // return the new content of the destination path this._propfind(err, dstRelPath, 1 /*depth*/, function(err, content) { next(err, { code: 201 /*Created*/, body: content }); }); }).bind(this)); } } else { /* unknown error */ setImmediate(next, err); } } else { /* dstPath exist */ if (overwriteParam) { // Destination resource already exists : destroy it first this._rmrf(dstPath, (function(err) { op(srcPath, dstPath, (function(err) { this._propfind(err, dstRelPath, 1 /*depth*/, function(err, content) { next(err, { code: 200 /*Ok*/, body: content }); }); }).bind(this)); }).bind(this)); } else { if (req.method.match(/MOVE/i) && (srcStat.ino === dstStat.ino) && (srcStat.mtime.getTime() === dstStat.mtime.getTime())) { op(srcPath, dstPath, (function(err) { this._propfind(err, dstRelPath, 1 /*depth*/, function(err, content) { next(err, { code: 200 /*Ok*/, body: content }); }); }).bind(this)); } else { setImmediate(next, new HttpError('Destination already exists', 412 /*Precondition-Failed*/)); } } } }.bind(this)); }; // XXX ENYO-1086: refactor tree walk-down FsLocal.prototype._cpr = function(srcPath, dstPath, next) { if (srcPath === dstPath) { setImmediate(next, new HttpError("Cannot copy on itself", 400 /*Bad Request*/)); return; } _copyNode(srcPath, dstPath, next); function _copyNode(srcPath, dstPath, next) { fs.stat(srcPath, function(err, stats) { if (err) { next(err); return; } if (stats.isDirectory()) { _copyDir(srcPath, dstPath, next); } else if (stats.isFile()){ copyFile(srcPath, dstPath, next); } }); } function _copyDir(srcPath, dstPath, next) { fs.readdir(srcPath, function(err, files) { if (err) { next(err); return; } fs.mkdir(dstPath, function(err) { if (err) { next(err); return; } async.forEachSeries(files, function(file, next) { _copyNode(path.join(srcPath, file), path.join(dstPath, file), next); }, next); }); }); } }; // module/main wrapper if (path.basename(process.argv[1], '.js') === basename) { // We are main.js: create & run the object... var knownOpts = { "root": path, "port": Number, "timeout": Number, "pathname": String, "level": ['silly', 'verbose', 'info', 'http', 'warn', 'error'], "help": Boolean }; var shortHands = { "r": "--root", "p": "--port", "t": "--timeout", "P": "--pathname", "l": "--level", "v": "--level verbose", "h": "--help" }; var opt = require('nopt')(knownOpts, shortHands, process.argv, 2 /*drop 'node' & basename*/); opt.level = opt.level || "http"; opt.pathname = opt.pathname || "/files"; opt.port = opt.port || 0; opt.timeout = opt.timeout || (2*60*1000); if (opt.help) { console.log("Usage: node " + basename + "\n" + " -p, --port port (o) local IP port of the express server (0: dynamic) [default: '0']\n" + " -t, --timeout milliseconds of inactivity before a server socket is presumed to have timed out [default: '120000']\n" + " -P, --pathname URL pathname prefix (before /deploy and /build [default: '/files']\n" + " -l, --level debug level ('silly', 'verbose', 'info', 'http', 'warn', 'error') [default: 'http']\n" + " -h, --help This message\n"); process.exit(0); } console.log("opt:", opt); log.level = opt.level; new FsLocal({ root: opt.root, pathname: opt.pathname, port: opt.port, timeout: opt.timeout, level: opt.level }, function(err, service){ if (err) { process.exit(err); } // process.send() is only available if the // parent-process is also node if (process.send) { process.send(service); } }); } else { module.exports = FsLocal; }