diff --git a/.gitmodules b/.gitmodules index 0b75bb95..0d5077d6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,9 +10,6 @@ [submodule "support/node-sftp"] path = support/node-sftp url = git://github.com/ajaxorg/node-sftp.git -[submodule "support/node-ssh"] - path = support/node-ssh - url = git://github.com/ajaxorg/node-ssh.git [submodule "support/node-ftp"] path = support/node-ftp url = git://github.com/ajaxorg/node-ftp.git diff --git a/README.md b/README.md index 7f5ecb6b..66e12376 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,49 @@ jsDAV jsDAV allows you to easily add WebDAV support to a NodeJS application. jsDAV is meant to cover the entire standard, and attempts to allow integration using an easy to understand API. -Short story ------------ - -TODO - -Usage ------ - -TODO - - +Features +-------- + + * Fully WebDAV compliant + * Supports Windows XP, Windows Vista, Mac OS/X, DavFSv2, Cadaver, Netdrive, Open Office, and probably more + * Supporting class 1, 2 and 3 Webdav servers + * Custom property support + +Features in development +----------------------- + + * Locking support (feature complete, but untested) + * Pass all Litmus tests + * CalDAV (to be tested with Evolution, iCal, iPhone and Lightning). + +Supported RFC's +--------------- + + * [RFC2617]: Basic/Digest auth + * [RFC2518]: First WebDAV spec + * [RFC4709]: [DavMount] + * [RFC5397]: current-user-principal + * [RFC5689]: Extended MKCOL + +RFC's in development +-------------------- + + * [RFC3744]: ACL + * [RFC4791]: CalDAV + * [RFC4918]: WebDAV revision + * CalDAV ctag, CalDAV-proxy + +[RFC2617]: http://www.ietf.org/rfc/rfc2617.txt +[RFC2518]: http://www.ietf.org/rfc/rfc2518.txt +[RFC3744]: http://www.ietf.org/rfc/rfc3744.txt +[RFC4709]: http://www.ietf.org/rfc/rfc4709.txt +[DavMount]: http://code.google.com/p/sabredav/wiki/DavMount +[RFC4791]: http://www.ietf.org/rfc/rfc4791.txt +[RFC4918]: http://www.ietf.org/rfc/rfc4918.txt +[RFC5397]: http://www.ietf.org/rfc/rfc5689.txt +[RFC5689]: http://www.ietf.org/rfc/rfc5689.txt + +See the [wiki](https://github.com/mikedeboer/jsDAV/wiki) for more information! Amsterdam, 2010. Mike de Boer. \ No newline at end of file diff --git a/lib/DAV/exceptions.js b/lib/DAV/exceptions.js index ca11ccb2..500e3011 100644 --- a/lib/DAV/exceptions.js +++ b/lib/DAV/exceptions.js @@ -79,7 +79,7 @@ exports.jsDAV_Exception = function(msg, extra) { } }; }; -exports.jsDAV_Exception.prototype = Error.prototype; +exports.jsDAV_Exception.prototype = new Error(); /** * BadRequest diff --git a/lib/DAV/ftp/directory.js b/lib/DAV/ftp/directory.js index e95e5127..8ffb9a32 100644 --- a/lib/DAV/ftp/directory.js +++ b/lib/DAV/ftp/directory.js @@ -19,7 +19,7 @@ var jsDAV = require("./../../jsdav"), Exc = require("./../exceptions"); function jsDAV_Ftp_Directory(path, ftp) { - this.path = (path || "").replace(/\s+[\/]+$/, ""); + this.path = path || ""; this.ftp = ftp; } @@ -38,7 +38,7 @@ exports.jsDAV_Ftp_Directory = jsDAV_Ftp_Directory; * @return void */ this.createFile = function(name, data, enc, cbftpcreatefile) { - var newPath = (this.path + "/" + name).replace(/[\/]+$/, ""); + var newPath = Util.trim(this.path + "/" + name, "/"); if (data.length === 0) { //ftp lib does not support writing empty files... data = new Buffer("empty file"); enc = "binary"; @@ -60,25 +60,20 @@ exports.jsDAV_Ftp_Directory = jsDAV_Ftp_Directory; * @return void */ this.createDirectory = function(name, cbftpcreatedir) { - var newPath = this.path + "/" + name.replace(/[\/]+$/, ""); + var newPath = Util.trim(this.path + "/" + name, "/"); var self = this; - var mkdir = this.ftp.mkdir(newPath, function(err) { + this.ftp.mkdir(newPath, function(err) { if (err) return cbftpcreatedir(err); - var chmod = self.ftp.chmod(newPath, 755, function(err) { - if (err) - return cbftpcreatedir(err); + self.ftp.chmod(newPath, 755, function(err) { + // Not strictly necessary, bypassing error... + /*if (err) + return cbftpcreatedir(err);*/ cbftpcreatedir(null, new jsDAV_Ftp_Directory(newPath, self.ftp)); }); - if (!chmod) - cbftpcreatedir(new Exc.jsDAV_Exception_NotImplemented("Could not create directory in " - + newPath + ". User not authorized or command CHMOD not implemented.")); }); - if (!mkdir) - cbftpcreatedir(new Exc.jsDAV_Exception_NotImplemented("Could not create directory in " - + newPath + ". User not authorized or command MKDIR not allowed.")); }; /** @@ -92,7 +87,7 @@ exports.jsDAV_Ftp_Directory = jsDAV_Ftp_Directory; if (typeof stat !== "object") return cbftpgetchild(new Exc.jsDAV_Exception_FileNotFound("Child node could not be retrieved")); - var path = (this.path + "/" + stat.name).replace(/[\/]+$/, ""); + var path = Util.trim(this.path + "/" + stat.name, "/"); if (this.ftp.$cache[path]) return cbftpgetchild(null, this.ftp.$cache[path]); @@ -112,25 +107,28 @@ exports.jsDAV_Ftp_Directory = jsDAV_Ftp_Directory; * @return Sabre_DAV_INode[] */ this.getChildren = function(cbftpgetchildren) { - var nodes = [], self = this; + var nodes = [], _self = this; this.ftp.readdir(this.path, function(err, listing) { if (err) return cbftpgetchildren(err); if (!listing) return cbftpgetchildren(null, nodes); - - Async.list(listing).each(function(node, next) { - self.getChild(node, function(err, node) { - if (err) - return next(); - - nodes.push(node); - next(); + + Async.list(listing) + .delay(0, 30) + .each(function(node, next) { + _self.getChild(node, function(err, node) { + if (err) + return next(); + + nodes.push(node); + next(); + }); + }) + .end(function() { + cbftpgetchildren(null, nodes); }); - }).end(function() { - cbftpgetchildren(null, nodes); - }); }); }; diff --git a/lib/DAV/ftp/file.js b/lib/DAV/ftp/file.js index 668c2ffb..c2ad3765 100644 --- a/lib/DAV/ftp/file.js +++ b/lib/DAV/ftp/file.js @@ -16,7 +16,7 @@ var jsDAV = require("./../../jsdav"), Util = require("./../util"); function jsDAV_Ftp_File(path, ftp) { - this.path = (path || "").replace(/[\/]+$/, ""); + this.path = path || ""; this.ftp = ftp; } @@ -41,7 +41,7 @@ exports.jsDAV_Ftp_File = jsDAV_Ftp_File; if (err) return cbftpput(err); // @todo what about parent node's cache?? - delete self.ftp.$cache[this.path]; + delete self.ftp.$cache[self.path]; cbftpput(); }); }; @@ -81,7 +81,7 @@ exports.jsDAV_Ftp_File = jsDAV_Ftp_File; */ this.getSize = function(cbftpgetsize) { var bytes = this.ftp.$cache[this.path].$stat.size; - cbftpgetsize(bytes); + cbftpgetsize(null, bytes); }; /** diff --git a/lib/DAV/ftp/node.js b/lib/DAV/ftp/node.js index 4fccfeef..3115ef16 100644 --- a/lib/DAV/ftp/node.js +++ b/lib/DAV/ftp/node.js @@ -85,7 +85,7 @@ exports.jsDAV_Ftp_Node = jsDAV_Ftp_Node; }; this.$isRoot = function() { - return this.hasFeature(jsDAV.__ICOLLECTION__) && this.path === ""; + return this.hasFeature(jsDAV.__ICOLLECTION__) && this.isRoot === true; }; /** diff --git a/lib/DAV/handler.js b/lib/DAV/handler.js index 60a1d257..7756b031 100644 --- a/lib/DAV/handler.js +++ b/lib/DAV/handler.js @@ -12,7 +12,6 @@ var Url = require("url"), Exc = require("./exceptions"), Util = require("./util"), Async = require("./../../support/async.js"), - StreamBuffer = Util.StreamBuffer, // DAV classes used directly by the Handler object jsDAV = require("./../jsdav"), @@ -35,7 +34,7 @@ var requestCounter = 0; */ function jsDAV_Handler(server, req, resp) { this.server = server; - this.httpRequest = new StreamBuffer(req); + this.httpRequest = Util.streamBuffer(req); this.httpResponse = resp; this.plugins = {}; @@ -1000,32 +999,14 @@ exports.NS_AJAXORG = "http://ajax.org/2005/aml"; : req.$data.toString(enc), cleanup); } - // HACK: MacOSX Finder and NodeJS just don't play nice. #$%^& - /*var timeout = setTimeout(function() { - if (jsDAV.debugMode) - console.log("ERROR: timeout for retrieving request body for " + req.url); - return cbreqbody(null, "", cleanup); - }, 5000);*/ - var timeout; - - if (!this.$getBodyStack) - this.$getBodyStack = []; - this.$getBodyStack.push([enc, cbreqbody]); - if (this.$reading) - return; - - this.$reading = true; - if (isStream) { var stream = []; - + req.streambuffer.ondata(function(data) { - clearTimeout(timeout); stream.push(data); }); req.streambuffer.onend(function() { - clearTimeout(timeout); _self.$reading = false; req.$data = Util.concatBuffers(stream); readDone(null, enc == "binary" @@ -1034,7 +1015,6 @@ exports.NS_AJAXORG = "http://ajax.org/2005/aml"; }); } else { - clearTimeout(timeout); var formidable = require("./../../support/formidable/lib/formidable"), form = new formidable.IncomingForm(); form.uploadDir = this.server.tmpDir; @@ -1055,12 +1035,7 @@ exports.NS_AJAXORG = "http://ajax.org/2005/aml"; } function readDone(err, data, cleanup) { - var args; - while (args = _self.$getBodyStack.shift()) { - enc = args[0]; - cbreqbody = args[1]; - cbreqbody(err, enc == "binary" ? data : data.toString(enc), cleanup); - } + cbreqbody(err, data, cleanup); } }; @@ -1387,7 +1362,7 @@ exports.NS_AJAXORG = "http://ajax.org/2005/aml"; this.getPropertiesForPath = function(path, propertyNames, depth, cbgetpropspath) { propertyNames = propertyNames || []; depth = depth || 0; - + if (depth != 0) depth = 1; @@ -1455,7 +1430,8 @@ exports.NS_AJAXORG = "http://ajax.org/2005/aml"; } Async.list(Object.keys(nodes)) - .each(function(myPath, cbnextpfp) { + .delay(0, 10) + .each(function(myPath, cbnextpfp) { var node = nodes[myPath]; var newProperties = { @@ -1500,6 +1476,7 @@ exports.NS_AJAXORG = "http://ajax.org/2005/aml"; // next loop! Async.list(propertyNames) + .delay(0, 10) .each(function(prop, cbnextprops) { if (typeof newProperties["200"][prop] != "undefined") return cbnextprops(); @@ -1589,6 +1566,7 @@ exports.NS_AJAXORG = "http://ajax.org/2005/aml"; .end(function(err) { if (!Util.empty(err)) return cbgetpropspath(err); + cbgetpropspath(null, returnPropertyList); }); } @@ -1703,15 +1681,14 @@ exports.NS_AJAXORG = "http://ajax.org/2005/aml"; if (!Util.empty(err)) return cbproppatchreq(err); var child, - firstChild = dom.firstChild, newProperties = {}, i = 0, - c = dom.firstChild.childNodes, + c = dom.childNodes, l = c.length; for (; i < l; ++i) { child = c[i]; operation = Util.toClarkNotation(child); - if (operation !== "{DAV:}set" && operation !== "{DAV:}remove") continue; + if (!operation || operation !== "{DAV:}set" && operation !== "{DAV:}remove") continue; innerProperties = Util.parseProperties(child, _self.propertyMap); for (propertyName in innerProperties) { @@ -1721,7 +1698,6 @@ exports.NS_AJAXORG = "http://ajax.org/2005/aml"; newProperties[propertyName] = propertyValue; } } - cbproppatchreq(null, newProperties); }); }; @@ -1749,13 +1725,14 @@ exports.NS_AJAXORG = "http://ajax.org/2005/aml"; // exceptions if it doesn't. var propertyName, status, props, _self = this, - result = { - "200" : [], - "403" : [], - "424" : [] - }, - remainingProperties = properties, + result = {}, + remainingProperties = Util.extend({}, properties), hasError = false; + result[uri] = { + "200" : [], + "403" : [], + "424" : [] + }; this.server.tree.getNodeForPath(uri, function(err, node) { if (!Util.empty(err)) @@ -1766,7 +1743,7 @@ exports.NS_AJAXORG = "http://ajax.org/2005/aml"; if (!node.hasFeature(jsDAV.__IPROPERTIES__)) { hasError = true; for (propertyName in properties) - Util.arrayRemove(result["403"]); + Util.arrayRemove(result[uri]["403"], propertyName); remainingProperties = {}; } @@ -1774,7 +1751,7 @@ exports.NS_AJAXORG = "http://ajax.org/2005/aml"; if (!hasError) { for (propertyName in properties) { if (_self.protectedProperties.indexOf(propertyName) > -1) { - Util.arrayRemove(result["403"]); + Util.arrayRemove(result[uri]["403"], propertyName); delete remainingProperties[propertyName]; hasError = true; } @@ -1789,13 +1766,13 @@ exports.NS_AJAXORG = "http://ajax.org/2005/aml"; if (updateResult === true) { // success for (propertyName in properties) - Util.arrayRemove(result["200"]); + Util.arrayRemove(result[uri]["200"], propertyName); } else if (updateResult === false) { // The node failed to update the properties for an // unknown reason foreach (propertyName in properties) - Util.arrayRemove(result["403"]); + Util.arrayRemove(result[uri]["403"], propertyName); } else if (typeof updateResult == "object") { // The node has detailed update information @@ -1804,22 +1781,21 @@ exports.NS_AJAXORG = "http://ajax.org/2005/aml"; else { return cbupdateprops(new Exc.jsDAV_Exception("Invalid result from updateProperties")); } - } for (propertyName in remainingProperties) { // if there are remaining properties, it must mean // there's a dependency failure - delete result["424"][propertyName]; + Util.arrayRemove(result[uri]["424"], propertyName); } // Removing empty array values - for (status in result) { - props = result[status]; + for (status in result[uri]) { + props = result[uri][status]; if (props.length === 0) - delete result[status]; + delete result[uri][status]; } - result["href"] = uri; + result[uri]["href"] = uri; cbupdateprops(null, result); }); }; diff --git a/lib/DAV/plugins/filelist.js b/lib/DAV/plugins/filelist.js index a93c30af..442e3363 100644 --- a/lib/DAV/plugins/filelist.js +++ b/lib/DAV/plugins/filelist.js @@ -9,9 +9,11 @@ var jsDAV = require("./../../jsdav"), jsDAV_ServerPlugin = require("./../plugin").jsDAV_ServerPlugin, jsDAV_Codesearch_Plugin = require("./codesearch"), + find = require("./../../find/find"), Spawn = require("child_process").spawn, Exc = require("./../exceptions"), - Util = require("./../util"); + Util = require("./../util"), + async = require("./../../../support/async.js"); function jsDAV_Filelist_Plugin(handler) { this.handler = handler; @@ -61,7 +63,8 @@ jsDAV_Filelist_Plugin.FIND_CMD = "find"; } return options; }; - + + /* this.doFilelist = function(node, options, cbsearch) { var args, _self = this; @@ -93,12 +96,42 @@ jsDAV_Filelist_Plugin.FIND_CMD = "find"; find.on("exit", function(code) { cbsearch(err, _self.parseSearchResult(out || "", node.path, options)); }); + };*/ + + this.doFilelist = function(node, options, cbsearch) { + var _self = this, + output = [], + re = new RegExp(".bzr|.cdv|.dep|.dot|.nib|.plst|.git|.hg|.pc|.svn|blib|CVS|RCS|SCCS|_darcs|_sgbak|autom4te\.cache|cover_db|_build|.tmp"); + + find(node.path, function(error, results) { + results.forEach(function(item) { + if (re.test(item)) + return; + else + output.push(item); + }); + cbsearch(error, _self.parseSearchResult(output, node.path, options)); + }); }; - + + this.doFilelist = function(node, options, cbsearch) { + var _self = this, + output = [], + re = new RegExp(".bzr|.cdv|.dep|.dot|.nib|.plst|.git|.hg|.pc|.svn|blib|CVS|RCS|SCCS|_darcs|_sgbak|autom4te\.cache|cover_db|_build|.tmp"); + + async.walkfiles(node.path, function(item) {return !re.test(item.path)}, async.PREORDER) + .each(function(item){ + output.push(item.path); + }) + .end(function(err, res) { + cbsearch(err, _self.parseSearchResult(output, node.path, options)); + }) + }; + this.parseSearchResult = function(res, basePath, options) { var namespace, prefix, lastFile, line, aXml = ['' + encodeURI(options.uri - + Util.rtrim(line.replace(basePath, "")), "/") - + ''); + line = encodeURI(options.uri + Util.rtrim(line.replace(basePath, "")), "/"); + if (line && line != '') + aXml.push('' + line + ''); } return aXml.join("") + ''; }; diff --git a/lib/DAV/plugins/ftp.js b/lib/DAV/plugins/ftp.js index a58cee9e..ec7483a2 100644 --- a/lib/DAV/plugins/ftp.js +++ b/lib/DAV/plugins/ftp.js @@ -5,9 +5,10 @@ * @author Luis Merino * @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License */ - + var jsDAV_ServerPlugin = require("./../plugin").jsDAV_ServerPlugin; var Exc = require("../exceptions"); +var Util = require("../util"); /** * Ftp Plugin @@ -28,50 +29,99 @@ function jsDAV_Ftp_Plugin(handler) { (function() { /** Redefines handler::invoke to delay request handling. This allows any middleware functionality like user authentication and whatnot * to be dealt with before the request gets dispatched, giving time to login and prepare the FTP server for ready state. - * + * * @throws jsDAV_Exception_Forbidden - * @return void + * @return void */ this.initialize = function() { - fixRootNodeUri(this.handler); - - var tree = this.handler.server.tree; - var conn = tree.ftp; - if (typeof conn == 'undefined' || tree.$ready) + var tree = this.tree = this.handler.server.tree; + + if (!tree.ftp) // Is FTP jsDav? return; - + + fixRootNodeUri(this.handler); + + // Use event to dispatch methods as stateful and make it high priority so this event is dispatched first. + this.handler.addEventListener( + "beforeMethod", + this.handleState.bind(this), + Util.EventEmitter.PRIO_HIGH, + tree.ftp.options.connTimeout + ); + + if (tree.$ready) { + if (!tree.ftp.$socket) { + // IDE was "disconnected", start process again + tree.$ready = false; + tree.ftp.connect(); + } + else return; + } + + // Once the Ftp server is ready, all instances of handler can be run after being halted var oldInvoke = this.handler.invoke, - self = this, + _self = this, args; - + this.handler.invoke = function(){ args = Array.prototype.slice.call(arguments); }; - - this.handler.server.tree.ftp.on('ftp.ready', function(oldInvoke, args) { + + this.handler.server.tree.ftp.once("ftp.ready", function(oldInvoke, args) { if (!tree.$ready) - console.info('Ftp connection ready on: '+ conn.options.host); + console.info('> Ftp connection ready on: '+ tree.ftp.options.host); tree.$ready = true; + // Assing old invoke method and call it back. this.invoke = oldInvoke; this.invoke.apply(this, args); }.bind(this.handler, oldInvoke, args)); - + + // When Ftp server has been initialized, error listener will be assigned, once. if (!tree.$inited) { - this.handler.server.tree.ftp.on('ftp.error', function(err) { - console.error('Ftp connection error on: '+ conn.options.host, err); - tree.$ready = false; - self.handler.handleError(new Exc.jsDAV_Exception_Forbidden("Could not establish connection with server on " - + conn.options.host +". Please make sure the initial path has the proper permissions and also that your credentials are correct.")); + this.handler.server.tree.ftp.on("ftp.error", function(err) { + console.error("> Ftp connection error on: " + tree.ftp.options.host, err); + _self.handler.handleError(new Exc.jsDAV_Exception_Forbidden("Could not establish connection with server on " + + tree.ftp.options.host +". Please make sure the initial path has the proper permissions and also that your credentials are correct.")); }); } - // Check if auth hasn't started check OR auth hasn't started but connecting has... - if (conn.$state !== null || tree.$inited) + // Checking if auth hasn't started but connecting has or the state has not been set yet... + if (tree.ftp.$state !== null || tree.$inited) return; - + tree.$inited = true; tree.initialize(); }; + /** + * This method intercepts requests and queues them to be executed when the next http response ends. + * + * @param {Object} e + * @return void + */ + this.handleState = function(e, method) { + var tree = this.tree; + + if (!tree.$line) + tree.$line = []; + else + tree.$line.push(e); + + var end = this.handler.httpResponse.end; + this.handler.httpResponse.end = function() { + if (tree.$line.length) + tree.$line.shift().next(); // call next + else + tree.$line = null; + + var args = Array.prototype.slice.call(arguments); + this.end = end; + this.end.apply(this, args); + }; + + if (!tree.$line.length) + e.next(); + }; + }).call(jsDAV_Ftp_Plugin.prototype = new jsDAV_ServerPlugin()); /** @description @@ -106,4 +156,4 @@ function fixRootNodeUri(handler) { }; } -module.exports = jsDAV_Ftp_Plugin; \ No newline at end of file +module.exports = jsDAV_Ftp_Plugin; diff --git a/lib/DAV/sftp/directory.js b/lib/DAV/sftp/directory.js index 64944e6f..493541f5 100644 --- a/lib/DAV/sftp/directory.js +++ b/lib/DAV/sftp/directory.js @@ -114,7 +114,15 @@ exports.jsDAV_SFTP_Directory = jsDAV_SFTP_Directory; * @return void */ this["delete"] = function(cbfsdel) { - this.sftp.rmdir(this.path, cbfsdel); + console.log("rm -Rf '" + this.path + "'"); + var child = this.sftp.spawn("rm -Rf '" + this.path + "'"); + var error = ''; + child.stderr.on('data', function(data){ + error += data.toString(); + }); + child.on('exit', function(){ + cbfsdel(error); + }); }; /** diff --git a/lib/DAV/tree/ftp.js b/lib/DAV/tree/ftp.js index e7b4d5e6..a6b42bdb 100644 --- a/lib/DAV/tree/ftp.js +++ b/lib/DAV/tree/ftp.js @@ -6,7 +6,8 @@ * @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License */ -var jsDAV_Tree = require("../tree").jsDAV_Tree, +var jsDAV = require("../../jsdav"), + jsDAV_Tree = require("../tree").jsDAV_Tree, jsDAV_Ftp_Directory = require("../ftp/directory").jsDAV_Ftp_Directory, jsDAV_Ftp_File = require("../ftp/file").jsDAV_Ftp_File, @@ -22,52 +23,95 @@ var jsDAV_Tree = require("../tree").jsDAV_Tree, * Creates this tree * Supply the path you'd like to share among with the options for the ftp connection * - * @param {String} basePath + * @param {Object} options * @contructor */ + +Ftp.debugMode = require("../../jsdav").debugMode; + function jsDAV_Tree_Ftp(options) { this.basePath = options.node || ""; this.options = options.ftp; - this.ftp = new Ftp(options.ftp); - this.ftp.$cache = {}; + this.setup(); } exports.jsDAV_Tree_Ftp = jsDAV_Tree_Ftp; (function() { - // Called by jsDAV_Ftp_Plugin. + this.setup = function() { + this.ftp = new Ftp(this.options); + this.ftp.$cache = {}; + }; + + /** + * This function is called from jsDAV_Ftp_Plugin as part of the initialization of the jsDav Tree. + * It aims to prepare FTP for the IDE; the next steps are taken: + * + * - Connects using specified host and port. + * - Auths the user with the specified username and password. + * - Determines the current working directory at the beginning of this connection. + * - Sends IDLE to update the idle timeout of the server if available, or defaults to 1 min otherwise + * - Stores a temp file under the initial path of the project. If Ftp refuses operations for the user, + * event "error" is emitted, if user lacks write permissions the state will be considered good anyways, + * and event – ftp.ready will be emitted; this will stab the callback execution. + * - Tries to execute MDTM to determine the lastMod of this temp file and stores it as a + * GMTDate (Date object). If this command is not implemented in the FTP server, this will stab the + * callback execution and delete the temp file. + * - Run stat (LIST) using the temp file path to determine is lastMod, usually returning the local date + * of the FTP server, which its used to foresee the timezone difference in hours. If this command fails + * there's something terribly wrong and this will stab the callback execution and delete the temp file. + * - The hour difference will be saved in the FTP instance for future reckoning and the "ftp.ready" event + * will be emitted finalising the process. + */ this.initialize = function() { var conn = this.ftp, user = this.options.user, pass = this.options.pass, - self = this; - - var tmpFile = this.getRealPath("io.c9.tmp"); + tmpFile, _self = this; - conn.addListener("connect", function() { + conn.on("connect", function() { conn.auth(user, pass, function(err) { if (err) return conn.emit("ftp.error", err); - conn.put(new Buffer("Cloud9 FTP connection test."), tmpFile, function(err) { + conn.pwd(function(err, workingDir) { if (err) return conn.emit("ftp.error", err); - conn.lastMod(tmpFile, function(err, date) { - if (err) - return conn.emit("ftp.error", err); - var GMTDate = date; - conn.stat(tmpFile, function(err, stat) { - if (err) - return conn.emit("ftp.error", err); - var localDate = stat.getLastMod(); - // Both dates were retrieved as GMT +0000 to make the comparison. - var timeDiff = localDate.getUTCHours() - GMTDate.getUTCHours(); - // Save FTP server LIST cmd difference in hours. - Ftp.TZHourDiff = timeDiff + 1; - conn["delete"](tmpFile, function(err) { - if (err) - return conn.emit("ftp.error", err); - conn.emit("ftp.ready"); + /** Preparing basePath for all ftp commands */ + _self.basePath = Util.rtrim(workingDir, "/") + "/" + Util.trim(_self.basePath, "/"); + /** Augment idle seconds of the server to 900 */ + var idleSeconds = 900; + conn.idle(idleSeconds, function() { // Attemps to change the idle seconds of the server connection, gracefully + tmpFile = _self.getRealPath("io.c9.tmp"); + conn.put(new Buffer("Cloud9 FTP connection test."), tmpFile, function(err) { + if (err) { + if (err.code == 550) // Server possibly throwed "Access is denied"...? + return conn.emit("error", err); + else + return conn.emit("ftp.ready"); + } + conn.lastMod(tmpFile, function(err, date) { + if (err) { + return deleteTempFile(function() { + conn.emit("ftp.ready"); + }); + } + var GMTDate = date; + conn.stat(tmpFile, function(err, stat) { + if (err) { + return deleteTempFile(function() { + conn.emit("error", err); + }); + } + var localDate = stat.getLastMod(); + // Both dates were retrieved as GMT +0000 to make the comparison. + var timeDiff = localDate.getUTCHours() - GMTDate.getUTCHours(); + // Save FTP server LIST cmd difference in hours. + conn.TZHourDiff = timeDiff + 1; + deleteTempFile(function() { + conn.emit("ftp.ready"); + }); + }); }); }); }); @@ -77,37 +121,55 @@ exports.jsDAV_Tree_Ftp = jsDAV_Tree_Ftp; conn.on("timeout", onServerFailed); conn.on("error", onServerFailed); - // conn.on("close", conn.connect); - // Failsafe connection. - try { conn.connect(); } catch(e) { onServerFailed(e.message); } + /** Failsafe connection start */ + try { + conn.connect(); + } catch(e) { + onServerFailed(e.message); + } + + function deleteTempFile(next) { + conn["delete"](tmpFile, function(err) { + if (err) + conn.emit("ftp.error", err); + next(); + }); + } + var connRetry = this.connRetry = null; + /** + * Any errors on the FTP server will end the connection, and try to reconnect after 5 seconds. + * This prevents connections hanging if an error is to occur. + */ function onServerFailed(err) { - conn.emit("ftp.error", err); - if (err/*.message && err.message.indexOf('ECONNRESET') > -1*/) { - //self.$executeNext(); - return conn.connect(); - } + conn.emit("ftp.error", (err ? err.message : err) + ". Connection retry in 5 secs ..."); + conn.end(); + connRetry = setTimeout(function(){ + conn.connect(); + }, 5000); } }; /** * Returns a new node for the given path * - * @param string path + * @param {String} path * @return void */ this.getNodeForPath = function(path, next) { - var self = this; var realPath = this.getRealPath(path); var conn = this.ftp; if (conn.$cache[realPath]) return next(null, conn.$cache[realPath]); - // Root node requires special treatment because it will not be listed. - if (realPath == "/" || realPath == "") - return next(null, conn.$cache["/"] = new jsDAV_Ftp_Directory("", conn)); + /** Root node requires special treatment because it will not be listed */ + if (realPath == this.basePath) { + var baseDir = new jsDAV_Ftp_Directory(realPath = Util.rtrim(realPath, "/"), conn); + baseDir.isRoot = true; + return next(null, conn.$cache[realPath] = baseDir); + } this.ftp.stat(realPath, function(err, stat) { if (!Util.empty(err)) @@ -118,6 +180,8 @@ exports.jsDAV_Tree_Ftp = jsDAV_Tree_Ftp; else conn.$cache[realPath] = new jsDAV_Ftp_File(realPath, conn); + // Hack for seconds since FTP "ls" doesn't return them. + stat.time.second = new Date().getSeconds(); conn.$cache[realPath].$stat = stat; next(null, conn.$cache[realPath]); }); @@ -126,13 +190,11 @@ exports.jsDAV_Tree_Ftp = jsDAV_Tree_Ftp; /** * Returns the real filesystem path for a webdav url. * - * @param string publicPath - * @return string + * @param {String} publicPath + * @return {String} */ this.getRealPath = function(publicPath) { var realPath = Util.rtrim(this.basePath, "/") + "/" + Util.trim(publicPath, "/"); - if (realPath != "/" && Util.rtrim(this.basePath, "/") == Util.rtrim(realPath, "/")) - realPath = Util.rtrim(realPath, "/"); return realPath; }; @@ -155,6 +217,11 @@ exports.jsDAV_Tree_Ftp = jsDAV_Tree_Ftp; /** * Moves a file or directory recursively. + * In case the ide crashed, the nodes cache was cleared and the source's parent will have to be pre-cached + * and so will have its children using $getParentNodeRecall(), this way we will be able to come back to this method + * to rename the source effectively. + * Once the MOVE has been executed, the node need's to be updated in the cache, and if it's a Directory type its + * children will have to be updated in the cache as well, so the new keys correspond to the new path. * * If the destination exists, delete it first. * @@ -165,18 +232,79 @@ exports.jsDAV_Tree_Ftp = jsDAV_Tree_Ftp; this.move = function(source, destination, next) { var source = this.getRealPath(source); var destination = this.getRealPath(destination); + var node = this.ftp.$cache[source]; + var _self = this; - var node = this.ftp.$cache[source]; - if (!node) - return next(new Exc.jsDAV_Exception_FileNotFound('Node was not found in the tree')); + if (!node) // This should only happen if the server crashed. + return this.$getParentNodeRecall(source); - var self = this; node.setName(destination, function(err){ if (err) return next(err); - self.ftp.$cache[destination] = node; - delete self.ftp.$cache[source]; + + _self.ftp.$cache[destination] = node; + delete _self.ftp.$cache[source]; + if (node.hasFeature(jsDAV.__ICOLLECTION__)) { + repathChildren.call(_self, source, destination); + } next(); }); + + function repathChildren(oldParentPath, newParentPath) { + var paths = Object.keys(this.ftp.$cache); + var re = new RegExp("^" + oldParentPath.replace(/\//g, "\\/") + ".+$"); + var path, node; + + for (var k in paths) { + path = paths[k]; + if (re.test(path)) { + node = this.ftp.$cache[path]; + delete this.ftp.$cache[path]; + path = newParentPath + path.substring(oldParentPath.length); + node.path = path; + this.ftp.$cache[path] = node; + } + } + } + }; + + /** + * Caches a path's parent path and its children, then goes back to the caller function with the + * same previous arguments. + * + * @param string path + * @return void + */ + this.$getParentNodeRecall = function(path) { + var caller = arguments.callee.caller; + var callerArgs = caller.arguments; + var next = callback = callerArgs[callerArgs.length-1]; + var parentPath = Util.splitPath(path)[0]; + var _self = this; + + this.getNodeForPath(parentPath.substring(this.basePath.length), function(err, node) { + if (err) + return next(err); + + node.getChildren(function(err, nodes) { + if (err) + return next(err); + + if (nodes.length && typeof caller === 'function') { + nodes.forEach(function(child) { + if (child.path === path) + callback = caller.bind.apply(caller, [_self].concat([].slice.call(callerArgs))); + }); + callback(); + } else + next(); + }); + }); + }; + + this.unmount = function() { + console.info("\r\nClosed connection to the server. Unmounting FTP tree."); + this.ftp.end(); + clearTimeout(this.connRetry); }; }).call(jsDAV_Tree_Ftp.prototype = new jsDAV_Tree()); diff --git a/lib/DAV/tree/sftp.js b/lib/DAV/tree/sftp.js index 520ac19a..8eb20645 100644 --- a/lib/DAV/tree/sftp.js +++ b/lib/DAV/tree/sftp.js @@ -95,9 +95,17 @@ exports.jsDAV_Tree_Sftp = jsDAV_Tree_Sftp; * @return void */ this.copy = function(source, destination, cbfscopy) { - this.sftp.exec("cp -Rf " + source + " " + destination, function(err, out) { - cbfscopy(err || out); - }); + console.log("cp -Rf '" + this.getRealPath(source) + "' '" + + this.getRealPath(destination) + "'"); + var child = this.sftp.spawn("cp -rf '" + this.getRealPath(source) + "' '" + + this.getRealPath(destination) + "'"); + var error = ''; + child.stderr.on('data', function(data){ + error += data.toString(); + }); + child.on('exit', function(){ + cbfscopy(error); + }); }; @@ -111,8 +119,9 @@ exports.jsDAV_Tree_Sftp = jsDAV_Tree_Sftp; * @return void */ this.move = function(source, destination, cbfsmove) { - this.sftp.exec("rm -Rf " + source + " " + destination, function(err, out) { - cbfscopy(err || out); + this.sftp.rename(this.getRealPath(source), this.getRealPath(destination), function(err) { + console.log(err); + cbfsmove(err); }); }; }).call(jsDAV_Tree_Sftp.prototype = new jsDAV_Tree()); diff --git a/lib/DAV/util.js b/lib/DAV/util.js index 87b8c02d..43e0f214 100644 --- a/lib/DAV/util.js +++ b/lib/DAV/util.js @@ -3,6 +3,7 @@ * @subpackage DAV * @copyright Copyright(c) 2011 Ajax.org B.V. * @author Mike de Boer + * @contributor Luis Merino * @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License */ var jsDAV = require("./../jsdav"), @@ -11,7 +12,7 @@ var jsDAV = require("./../jsdav"), Path = require("path"), Async = require("./../../support/async.js"), Exc = require("./exceptions"); - + if (process.version.split(".")[1] > 2) var Xml = require("./../../support/node-o3-xml-v4/lib/o3-xml"); else @@ -220,7 +221,7 @@ exports.escapeRegExp = function(str) { }; exports.escapeShell = function(str) { - return str.replace(/([\\\/"$])/g, '\\$1'); + return str.replace(/([\\"'`$\s])/g, "\\$1"); }; // Internationalization strings @@ -340,9 +341,9 @@ exports.dateFormat = (function () { TT : H < 12 ? "AM" : "PM", Z : utc ? "UTC" - : (String(date).match(timezone) + : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""), - o : (o > 0 ? "-" : "+") + o : (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), S : ["th", "st", "nd", "rd"] @@ -463,7 +464,7 @@ exports.xmlEntityMap = { * @return {String} the escaped string. */ exports.escapeXml = function(str) { - + return (str || "") .replace(/&/g, "&") .replace(/"/g, """) @@ -515,17 +516,17 @@ exports.loadDOMDocument = function(xml, callback) { exports.xmlParseError = function(xml){ //if (xml.documentElement.tagName == "parsererror") { - if (xml.getElementsByTagName("parsererror").length) { + if (xml.getElementsByTagName("parsererror").length) { console.log("ATTENTION::: we actually HAVE an XML error :) "); var str = xml.documentElement.firstChild.nodeValue.split("\n"), linenr = str[2].match(/\w+ (\d+)/)[1], message = str[0].replace(/\w+ \w+ \w+: (.*)/, "$1"), - + srcText = xml.documentElement.lastChild.firstChild.nodeValue;//.split("\n")[0]; - throw new Error("XML Parse Error on line " + linenr, message + + throw new Error("XML Parse Error on line " + linenr, message + "\nSource Text : " + srcText.replace(/\t/gi, " ")); } - + return xml; }; @@ -907,17 +908,36 @@ exports.uniqid = function(prefix, more_entropy) { exports.EventEmitter = function() {}; exports.EventEmitter.DEFAULT_TIMEOUT = 2000; // in milliseconds +exports.EventEmitter.PRIO_LOW = 0x0001; +exports.EventEmitter.PRIO_NORMAL = 0x0002; +exports.EventEmitter.PRIO_HIGH = 0x0004; (function() { - var _slice = Array.prototype.slice; + var _slice = Array.prototype.slice, + _ev = exports.EventEmitter; + + function persistRegistry() { + if (this.$eventRegistry) + return; + this.$eventRegistry = {}; + this.$eventRegistry[_ev.PRIO_LOW] = {}; + this.$eventRegistry[_ev.PRIO_NORMAL] = {}; + this.$eventRegistry[_ev.PRIO_HIGH] = {}; + } + + function getListeners(eventName) { + return (this.$eventRegistry[_ev.PRIO_HIGH][eventName] || []).concat( + this.$eventRegistry[_ev.PRIO_NORMAL][eventName] || []).concat( + this.$eventRegistry[_ev.PRIO_LOW][eventName] || []); + } this.dispatchEvent = function() { - this.$eventRegistry = this.$eventRegistry || {}; + persistRegistry.call(this); var e, args = _slice.call(arguments), eventName = args.shift().toLowerCase(), - listeners = this.$eventRegistry[eventName] || [], + listeners = getListeners.call(this, eventName), cbdispatch = (typeof args[args.length - 1] == "function") ? args.pop() : function(){}; @@ -943,8 +963,9 @@ exports.EventEmitter.DEFAULT_TIMEOUT = 2000; // in milliseconds }); }; - this.addEventListener = function(eventName, listener, timeout) { - this.$eventRegistry = this.$eventRegistry || {}; + this.addEventListener = function(eventName, listener, prio, timeout) { + persistRegistry.call(this); + listener.$usetimeout = timeout === false ? 0 : (typeof timeout == "number") @@ -952,23 +973,28 @@ exports.EventEmitter.DEFAULT_TIMEOUT = 2000; // in milliseconds : exports.EventEmitter.DEFAULT_TIMEOUT; eventName = eventName.toLowerCase(); - var listeners = this.$eventRegistry[eventName]; + prio = prio || _ev.PRIO_NORMAL; + var allListeners = getListeners.call(this, eventName), + listeners = this.$eventRegistry[prio][eventName]; if (!listeners) - listeners = this.$eventRegistry[eventName] = []; - if (listeners.indexOf(listener) == -1) + listeners = this.$eventRegistry[prio][eventName] = []; + if (allListeners.indexOf(listener) == -1) listeners.push(listener); }; this.removeEventListener = function(eventName, listener) { - this.$eventRegistry = this.$eventRegistry || {}; + persistRegistry.call(this); eventName = eventName.toLowerCase(); - var listeners = this.$eventRegistry[eventName]; - if (!listeners) - return; - var index = listeners.indexOf(listener); - if (index !== -1) - listeners.splice(index, 1); + var _self = this; + [_ev.PRIO_LOW, _ev.PRIO_NORMAL, _ev.PRIO_HIGH].forEach(function(prio) { + var listeners = _self.$eventRegistry[prio][eventName]; + if (!listeners) + return; + var index = listeners.indexOf(listener); + if (index !== -1) + listeners.splice(index, 1); + }); }; }).call(exports.EventEmitter.prototype); @@ -994,59 +1020,72 @@ exports.concatBuffers = function(bufs) { var buffer, length = 0, index = 0; - + if (!Array.isArray(bufs)) bufs = Array.prototype.slice.call(arguments); for (var i = 0, l = bufs.length; i < l; ++i) { buffer = bufs[i]; + if (!buffer) + continue; + if (!Buffer.isBuffer(buffer)) buffer = bufs[i] = new Buffer(buffer); length += buffer.length; } buffer = new Buffer(length); - + bufs.forEach(function(buf, i) { buf = bufs[i]; buf.copy(buffer, index, 0, buf.length); index += buf.length; delete bufs[i]; }); - + return buffer; }; -exports.StreamBuffer = function(req) { +/** + * StreamBuffer - Buffers submitted data in advance to facilitate asynchonous operations + * @based on Richard Rodger's idea + * @author Luis Merino + */ +exports.streamBuffer = function(req) { var buffers = []; - var ended = false; - var ondata = onend = null; - - this.ondata = function(fn) { - for(var i = 0; i < buffers.length; i++) - fn(buffers[i]); - ondata = fn; + var ended = false; + var ondata = null; + var onend = null; + + req.streambuffer = { + ondata: function(fn) { + for (var i = 0; i < buffers.length; i++) + fn(buffers[i]); + ondata = fn; + buffers = null; + }, + + onend: function(fn) { + onend = fn; + if (ended) + onend(); + } }; - this.onend = function(fn) { - onend = fn; - if (ended) - onend(); - }; + req.on("data", function(chunk) { + if (!chunk) + return; - req.on('data', function(chunk) { if (ondata) ondata(chunk); else buffers.push(chunk); }); - req.on('end', function() { + req.on("end", function() { ended = true; if (onend) onend(); }); - req.streambuffer = this; - return req; -}; \ No newline at end of file +}; diff --git a/lib/find/async-map.js b/lib/find/async-map.js new file mode 100644 index 00000000..c86f2452 --- /dev/null +++ b/lib/find/async-map.js @@ -0,0 +1,58 @@ +/* +usage: + +// do something to a list of things +asyncMap(myListOfStuff, function (thing, cb) { doSomething(thing.foo, cb) }, cb) +// do more than one thing to each item +asyncMap(list, fooFn, barFn, cb) +// call a function that needs to go in and call the cb 3 times +asyncMap(list, callsMoreThanOnce, 3, cb) + +*/ + +module.exports = asyncMap + +function asyncMap () { + var steps = Array.prototype.slice.call(arguments) + , list = steps.shift() || [] + , cb_ = steps.pop() + if (typeof cb_ !== "function") throw new Error( + "No callback provided to asyncMap") + if (!list) return cb_(null, []) + if (!Array.isArray(list)) list = [list] + var n = steps.length + , data = [] // actually a 2d array + , errState = null + , l = list.length + , a = l * n + if (!a) return cb_(null, []) + function cb (er) { + if (errState) return + var argLen = arguments.length + for (var i = 1; i < argLen; i ++) if (arguments[i] !== undefined) { + data[i - 1] = (data[i - 1] || []).concat(arguments[i]) + } + // see if any new things have been added. + if (list.length > l) { + var newList = list.slice(l) + a += (list.length - l) * n + l = list.length + process.nextTick(function () { + newList.forEach(function (ar) { + steps.forEach(function (fn) { fn(ar, cb) }) + }) + }) + } + // allow the callback to return boolean "false" to indicate + // that an error should not tank the process. + if (er || --a === 0) { + errState = er + cb_.apply(null, [errState].concat(data)) + } + } + // expect the supplied cb function to be called + // "n" times for each thing in the array. + list.forEach(function (ar) { + steps.forEach(function (fn) { fn(ar, cb) }) + }) +} \ No newline at end of file diff --git a/lib/find/find.js b/lib/find/find.js new file mode 100644 index 00000000..0a32dbee --- /dev/null +++ b/lib/find/find.js @@ -0,0 +1,79 @@ +// walks a set of directories recursively, and returns +// the list of files that match the filter, if one is +// provided. + +module.exports = find; + +var fs = require("./graceful-fs"), + asyncMap = require("./async-map"), + path = require("path"); + +function find (dir, filter, depth, cb) { + if (typeof cb !== "function") { + cb = depth; + depth = Infinity; + } + if (typeof cb !== "function") { + cb = filter; + filter = null; + } + + if (filter instanceof RegExp) filter = reFilter(filter); + if (typeof filter === "string") filter = strFilter(filter); + if (!Array.isArray(dir)) dir = [dir]; + if (!filter) filter = nullFilter; + asyncMap(dir, findDir(filter, depth), cb); +} + +function findDir (filter, depth) { + return function (dir, cb) { + fs.lstat(dir, function (er, stats) { + // don't include missing files, but don't abort either + if (er) return cb(); + if (!stats.isDirectory()) return findFile(dir, filter, depth)("", cb); + var found = []; + //if (!filter || filter(dir, "dir")) found.push(dir+"/"); + if (dir[dir.length - 1] == '/') + found.push(dir); + else + found.push(dir+"/"); + if (depth <= 0) return cb(null, found); + cb = (function (cb) { + return function (er, f) { + cb(er, found.concat(f)); + }; + })(cb); + fs.readdir(dir, function (er, files) { + if (er) return cb(er); + asyncMap(files, findFile(dir, filter, depth - 1), cb); + }); + }); + }; +} + +function findFile (dir, filter, depth) { + return function (f, cb) { + f = path.join(dir, f); + fs.lstat(f, function (er, s) { + // don't include missing files, but don't abort either + if (er) return cb(); + if (s.isDirectory()) return find(f, filter, depth, cb); + if (!filter || filter(f, "file")) cb(null, f); + else cb(); + }); + }; +} + +function reFilter (re) { + return function (f, type) { + return nullFilter(f, type) && f.match(re); + }; +} + +function strFilter (s) { + return function (f, type) { + return nullFilter(f, type) && f.indexOf(s) === 0; + }; +} + +function nullFilter (f, type) { return type === "file" && f; } \ No newline at end of file diff --git a/lib/find/graceful-fs.js b/lib/find/graceful-fs.js new file mode 100644 index 00000000..3463e8a4 --- /dev/null +++ b/lib/find/graceful-fs.js @@ -0,0 +1,33 @@ +// wrapper around the non-sync fs functions to gracefully handle +// having too many file descriptors open. Note that this is +// *only* possible because async patterns let one interject timeouts +// and other cleverness anywhere in the process without disrupting +// anything else. +var fs = require("fs") + , timeout = 0 + +Object.keys(fs) + .forEach(function (i) { + exports[i] = (typeof fs[i] !== "function") ? fs[i] + : (i.match(/^[A-Z]|^create|Sync$/)) ? function () { + return fs[i].apply(fs, arguments) + } + : graceful(fs[i]) + }) + +function graceful (fn) { return function GRACEFUL () { + var args = Array.prototype.slice.call(arguments) + , cb_ = args.pop() + args.push(cb) + function cb (er) { + if (er && er.message.match(/^EMFILE, Too many open files/)) { + setTimeout(function () { + GRACEFUL.apply(fs, args) + }, timeout ++) + return + } + timer = 0 + cb_.apply(null, arguments) + } + fn.apply(fs, args) +}} \ No newline at end of file diff --git a/support/node-sftp b/support/node-sftp index ea34bb02..56422e47 160000 --- a/support/node-sftp +++ b/support/node-sftp @@ -1 +1 @@ -Subproject commit ea34bb02332bad9acc6c716d2311f83b615160cb +Subproject commit 56422e47432270fafc9dd2d840175f1679b2ae7c diff --git a/support/node-ssh b/support/node-ssh deleted file mode 160000 index f1742900..00000000 --- a/support/node-ssh +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f17429002c4212bf243d12ad25e06cf0628d5b75 diff --git a/test/litmus_osx64/litmus b/test/litmus_osx64/litmus new file mode 100755 index 00000000..08762d06 --- /dev/null +++ b/test/litmus_osx64/litmus @@ -0,0 +1,59 @@ +#!/bin/sh +# Copyright (c) 2001-2005, 2008 Joe Orton + +prefix=/usr/local +exec_prefix=/usr/local +libexecdir=${exec_prefix}/libexec +datadir=${datarootdir} +datarootdir=${prefix}/share + +HTDOCS=${HTDOCS-"${datarootdir}/litmus/htdocs"} +TESTROOT=${TESTROOT-"${exec_prefix}/libexec/litmus"} +TESTS=${TESTS-"basic copymove props locks http"} + +usage() { + cat <. +EOF + exit 1 +} + +nofail=0 + +case $1 in +--help|-h) usage ;; +--keep-going|-k) nofail=1; shift ;; +--version) echo litmus 0.12.1; exit 0 ;; +esac + +test "$#" = "0" && usage + +for t in $TESTS; do + tprog="${TESTROOT}/${t}" + if test -x ${tprog}; then + if ${tprog} --htdocs ${HTDOCS} "$@"; then + : pass + elif test $nofail -eq 0; then + echo "See debug.log for network/debug traces." + exit 1 + fi + else + echo "ERROR: Could not find ${tprog}" + exit 1 + fi +done diff --git a/test/test_ftp.js b/test/test_ftp.js new file mode 100644 index 00000000..f1792ea4 --- /dev/null +++ b/test/test_ftp.js @@ -0,0 +1,212 @@ + +var assert = require("assert"); +var Fs = require("fs"); +var exec = require('child_process').spawn; +var jsDAV = require("./../lib/jsdav"); +var Http = require("http"); +var _ = require("../support/node-ftp/support/underscore"); + +var _c = { + host: "www.linhnguyen.nl", + username: "cloud9", + pass: "cloud9", + port: 21 +}; + +jsDAV.debugMode = true; + +module.exports = { + timeout: 30000, + + setUpSuite: function(next) { + //exec('/bin/launchctl', ['load', '-w', '/System/Library/LaunchDaemons/ftp.plist']); + + var server = this.server = jsDAV.createServer({ + type: "ftp", + node: "/c9", + ftp: { + host: _c.host, + user: _c.username, + pass: _c.pass, + port: _c.port + } + }, 8000); + + this.ftp = server.tree.ftp; + next(); + }, + + tearDownSuite: function(next) { + //exec('/bin/launchctl', ['unload', '-w', '/System/Library/LaunchDaemons/ftp.plist']); + + this.server.tree.unmount(); + this.server = null; + next(); + }, + + "test Ftp connect": function(next) { + var self = this; + function assertError(e) { + if (e) throw e; + else throw new Error("FTP timed out.") + }; + + this.ftp.on("connect", function() { + next(); + }); + this.ftp.on("error", assertError) + this.ftp.on("timeout", assertError) + + this.ftp.connect(_c.port, _c.host); + }, + + "test User logs in, lists root directory, logs out": function(next) { + var _self = this; + this.ftp.auth(_c.username, _c.pass, function(err) { + assert.ok(!err); + _self.ftp.readdir("/", function(err, nodes) { + assert.ok(!err); + assert.ok(_.isArray(nodes)); + next(); + }); + }); + }, + /** + * 3rd Scenario + * User logs in and lists (first PROPFIND of basePath) + * creates a folder in root + * creates a file in it + * creates a second folder in root + * moves first folder in second + * logs out + */ + ">test Stateless requests on jsDav Ftp": function(next) { + var _self = this; + var options = _.extend(this.getHttpReqOptions(), { + path: "/", + method: "PROPFIND" + }); + var request = Http.request(options); + request.write(''); + request.on("response", function(res) { + res.on("data", function(buff) { + xmlResponse = buff.toString(); + assert.ok(xmlResponse.indexOf("HTTP/1.1 200 Ok") > -1); + afterPropfind.call(_self); + }); + }); + request.end(); + + function afterPropfind() { + var successes = 0, + _self = this; + // Request #1: creates a folder in root + setTimeout(function() { + options = _.extend(_self.getHttpReqOptions(["content-length"]), { + path: "/New_Ftp_Folder", + method: "MKCOL", + headers: { + "content-length": 0 + } + }); + Http.request(options, function(res) { + assert.equal(res.statusCode, 201); + successes++; + }).end(); + }, 100); + // Request #2: creates a file in it + setTimeout(function() { + options = _.extend(_self.getHttpReqOptions(["content-length", "content-type"]), { + path: "/New_Ftp_Folder/Untitled.js", + method: "PUT", + headers: { + "content-length": 0, + "content-type": "text/plain" + } + }); + Http.request(options, function(res) { + assert.equal(res.statusCode, 201); + successes++; + }).end(); + }, 200); + // Request #3: create a second folder in root + setTimeout(function() { + options = _.extend(_self.getHttpReqOptions(["content-length"]), { + path: "/New_Ftp_Folder_2", + method: "MKCOL", + headers: { + "content-length": 0 + } + }); + Http.request(options, function(res) { + assert.equal(res.statusCode, 201); + successes++; + }).end(); + }, 300); + // Request #4: moves first folder into second + setTimeout(function(){ + options = _.extend(_self.getHttpReqOptions(["content-length"]), { + path: "/New_Ftp_Folder", + method: "MOVE", + headers: { + "destination": "/New_Ftp_Folder_2/New_Ftp_Folder", + "content-length": 0 + } + }); + Http.request(options, function(res) { + assert.equal(res.statusCode, 201); + successes++; + }).end(); + }, 400); // give a little time to run the two MKCOL requests first. + var loop = setInterval(function() { + console.log('Interval: Number of responses back...', successes); + if (successes >= 4) { + clearInterval(loop); + next(); + } + }, 1000); + } + }, + + getHttpReqOptions: function(exclude_headers, exclude) { + var options = { + host: "127.0.0.1", + port: 8000, + headers: { + "accept": "*/*", + //"Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.3", + //"Accept-Encoding": "gzip,deflate,sdch", + //"Accept-Language": "en-US,en;q=0.8", + "connection": "keep-alive", + "content-Length": 92, + "content-type": "text/xml; charset=UTF-8", + //"depth": 1, + //"host": "localhost:5000", + //"Origin": "http://localhost:5000", + //"Referer": "http://localhost:5000/luismerino/ftp" + //"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1", + //"X-Requested-With": "XMLHttpRequest" + } + }; + if (_.isArray(exclude_headers)) { + Object.keys(options.headers).forEach(function(key) { + if (exclude_headers.indexOf(key) > -1) + delete options.headers[key]; + }); + } + if (_.isArray(exclude)) { + Object.keys(options).forEach(function(key) { + if (exclude.indexOf(key) > -1) + delete options[key]; + }); + } + return options; + } +}; + +process.on("exit", function() { + if (module.exports.conn) + module.exports.conn.end(); +}); + +!module.parent && require("./../support/async.js/lib/test").testcase(module.exports, "FTP"/*, timeout*/).exec();