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();