Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

added sftp backend support

  • Loading branch information...
commit 64f11b9489e4ab7bec70f95adf66ddbc26cfd7dc 1 parent 417141e
@mikedeboer authored
View
27 lib/DAV/server.js
@@ -49,19 +49,26 @@ function Server(options) {
this.debugExceptions = exports.debugMode;
if (options && typeof options.standalone == "undefined")
options.standalone = true;
+ this.options = options;
if (options && typeof options.tree == "object" && options.tree.hasFeature(jsDAV.__TREE__)) {
this.tree = options.tree;
}
else if (options && typeof options.node == "object" && options.node.hasFeature(jsDAV.__INODE__)) {
- this.tree = new jsDAV_ObjectTree(options.node);
+ this.tree = new jsDAV_ObjectTree(options.node, options);
}
else if (options && typeof options.node == "string" && options.node.indexOf("/") > -1) {
- this.tree = new jsDAV_Tree_Filesystem(options.node);
+ this.tree = new jsDAV_Tree_Filesystem(options.node, options);
+ }
+ else if (options && typeof options.type == "string") {
+ if (options.type == "sftp") {
+ var jsDAV_Tree_Sftp = require("./tree/sftp").jsDAV_Tree_Sftp;
+ this.tree = new jsDAV_Tree_Sftp(options);
+ }
}
else if (!options) {
var root = new jsDAV_SimpleDirectory("root");
- this.tree = new jsDAV_ObjectTree(root);
+ this.tree = new jsDAV_ObjectTree(root, options);
}
else {
throw new Exc.jsDAV_Exception("Invalid argument passed to constructor. "
@@ -225,11 +232,11 @@ exports.createServer = function(options, port, host) {
return server;
};
-exports.mount = function(path, mountpoint, server, standalone) {
- return new Server({
- node : path,
- mount : mountpoint,
- server : server,
- standalone: standalone
- });
+exports.mount = function(options) {
+ var s = new Server(options);
+ s.unmount = function() {
+ if (this.tree.unmount)
+ this.tree.unmount();
+ };
+ return s;
};
View
129 lib/DAV/sftp/directory.js
@@ -0,0 +1,129 @@
+/*
+ * @package jsDAV
+ * @subpackage DAV
+ * @copyright Copyright (C) 2010 Mike de Boer. All rights reserved.
+ * @author Mike de Boer <mike AT ajax DOT org>
+ * @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License
+ */
+
+var jsDAV = require("./../../jsdav"),
+ jsDAV_SFTP_Node = require("./node").jsDAV_SFTP_Node,
+ jsDAV_SFTP_File = require("./file").jsDAV_SFTP_File,
+ jsDAV_Directory = require("./../directory").jsDAV_Directory,
+ jsDAV_iCollection = require("./../iCollection").jsDAV_iCollection,
+ jsDAV_iQuota = require("./../iQuota").jsDAV_iQuota,
+
+ Fs = require("fs"),
+ Async = require("./../../../support/async.js"),
+ Exc = require("./../exceptions");
+
+function jsDAV_SFTP_Directory(path, sftp) {
+ this.path = (path || "").replace(/[\/]+$/, "");
+ this.sftp = sftp;
+}
+
+exports.jsDAV_SFTP_Directory = jsDAV_SFTP_Directory;
+
+(function() {
+ this.implement(jsDAV_Directory, jsDAV_iCollection, jsDAV_iQuota);
+
+ /**
+ * Creates a new file in the directory
+ *
+ * data is a readable stream resource
+ *
+ * @param string name Name of the file
+ * @param resource data Initial payload
+ * @return void
+ */
+ this.createFile = function(name, data, enc, cbfscreatefile) {
+ var newPath = (this.path + "/" + name).replace(/[\/]+$/, "");
+ if (data.length === 0) { //sftp lib does not support writing empty files...
+ data = new Buffer("empty file");
+ enc = "binary";
+ }
+ this.sftp.writeFile(newPath, data, enc || "utf8", cbfscreatefile);
+ };
+
+ /**
+ * Creates a new subdirectory
+ *
+ * @param string name
+ * @return void
+ */
+ this.createDirectory = function(name, cbfscreatedir) {
+ var newPath = this.path + "/" + name.replace(/[\/]+$/, "");
+ this.sftp.mkdir(newPath, 0755, cbfscreatedir);
+ };
+
+ /**
+ * Returns a specific child node, referenced by its name
+ *
+ * @param string name
+ * @throws Sabre_DAV_Exception_FileNotFound
+ * @return Sabre_DAV_INode
+ */
+ this.getChild = function(name, cbfsgetchild) {
+ var path = (this.path + "/" + name).replace(/[\/]+$/, ""),
+ _self = this;
+
+ this.sftp.stat(path, function(err, stat) {
+ if (err || typeof stat == "undefined") {
+ return cbfsgetchild(new Exc.jsDAV_Exception_FileNotFound("File with name "
+ + path + " could not be located"));
+ }
+ cbfsgetchild(null, stat.isDirectory()
+ ? new jsDAV_SFTP_Directory(path, _self.sftp)
+ : new jsDAV_SFTP_File(path, _self.sftp))
+ });
+ };
+
+ /**
+ * Returns an array with all the child nodes
+ *
+ * @return Sabre_DAV_INode[]
+ */
+ this.getChildren = function(cbfsgetchildren) {
+ var nodes = [],
+ _self = this;
+ this.sftp.readdir(this.path, function(err, listing) {
+ if (err)
+ return cbfsgetchildren(null, nodes);
+ Async.list(listing)
+ .each(function(node, cbnext) {
+ var path = (_self.path + "/" + node).replace(/[\/]+$/, "");
+ _self.sftp.stat(path, function(err, stat) {
+ if (err)
+ return cbnext();
+ nodes.push(stat.isDirectory()
+ ? new jsDAV_SFTP_Directory(path, _self.sftp)
+ : new jsDAV_SFTP_File(path, _self.sftp)
+ );
+ cbnext();
+ });
+ })
+ .end(function() {
+ cbfsgetchildren(null, nodes);
+ });
+ });
+ };
+
+ /**
+ * Deletes all files in this directory, and then itself
+ *
+ * @return void
+ */
+ this["delete"] = function(cbfsdel) {
+ this.sftp.rmdir(this.path, cbfsdel);
+ };
+
+ /**
+ * Returns available diskspace information
+ *
+ * @return array
+ */
+ this.getQuotaInfo = function(cbfsquota) {
+ // @todo: impl. sftp.statvfs();
+ return cbfsquota(null, [0, 0]);
+ };
+}).call(jsDAV_SFTP_Directory.prototype = new jsDAV_SFTP_Node());
View
107 lib/DAV/sftp/file.js
@@ -0,0 +1,107 @@
+/*
+ * @package jsDAV
+ * @subpackage DAV
+ * @copyright Copyright (C) 2010 Mike de Boer. All rights reserved.
+ * @author Mike de Boer <mike AT ajax DOT org>
+ * @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License
+ */
+
+var jsDAV = require("./../../jsdav"),
+ jsDAV_SFTP_Node = require("./node").jsDAV_SFTP_Node,
+ jsDAV_Directory = require("./../directory").jsDAV_Directory,
+ jsDAV_iFile = require("./../iFile").jsDAV_iFile,
+
+ Fs = require("fs"),
+ Exc = require("./../exceptions"),
+ Util = require("./../util");
+
+function jsDAV_SFTP_File(path, sftp) {
+ this.path = (path || "").replace(/[\/]+$/, "");
+ this.sftp = sftp;
+}
+
+exports.jsDAV_SFTP_File = jsDAV_SFTP_File;
+
+(function() {
+ this.implement(jsDAV_iFile);
+
+ /**
+ * Updates the data
+ *
+ * @param {mixed} data
+ * @return void
+ */
+ this.put = function(data, type, cbfsput) {
+ this.sftp.writeFile(this.path, data, type || "utf8", cbfsput);
+ };
+
+ /**
+ * Returns the data
+ *
+ * @return Buffer
+ */
+ this.get = function(cbfsfileget) {
+ if (this.$buffer)
+ return cbfsfileget(null, this.$buffer);
+ var _self = this;
+ this.sftp.readFile(this.path, null, function(err, buff) {
+ if (err)
+ return cbfsfileget(err);
+ // Zero length buffers act funny, use a string
+ if (buff.length === 0)
+ buff = "";
+ //_self.$buffer = buff;
+ cbfsfileget(null, buff);
+ });
+ };
+
+ /**
+ * Delete the current file
+ *
+ * @return void
+ */
+ this["delete"] = function(cbfsfiledel) {
+ this.sftp.unlink(this.path, cbfsfiledel);
+ };
+
+ /**
+ * Returns the size of the node, in bytes
+ *
+ * @return int
+ */
+ this.getSize = function(cbfsgetsize) {
+ if (this.$stat)
+ return cbfsgetsize(null, this.$stat.size);
+ var _self = this;
+ this.sftp.stat(this.path, function(err, stat) {
+ if (err || !stat) {
+ return cbfsgetsize(new Exc.jsDAV_Exception_FileNotFound("File at location "
+ + _self.path + " not found"));
+ }
+ //_self.$stat = stat;
+ cbfsgetsize(null, stat.size);
+ });
+ };
+
+ /**
+ * Returns the ETag for a file
+ * An ETag is a unique identifier representing the current version of the file.
+ * If the file changes, the ETag MUST change.
+ * Return null if the ETag can not effectively be determined
+ *
+ * @return mixed
+ */
+ this.getETag = function(cbfsgetetag) {
+ cbfsgetetag(null, null);
+ };
+
+ /**
+ * Returns the mime-type for a file
+ * If null is returned, we'll assume application/octet-stream
+ *
+ * @return mixed
+ */
+ this.getContentType = function(cbfsmime) {
+ return cbfsmime(null, Util.mime.type(this.path));
+ };
+}).call(jsDAV_SFTP_File.prototype = new jsDAV_SFTP_Node());
View
81 lib/DAV/sftp/node.js
@@ -0,0 +1,81 @@
+/*
+ * @package jsDAV
+ * @subpackage DAV
+ * @copyright Copyright (C) 2010 Mike de Boer. All rights reserved.
+ * @author Mike de Boer <mike AT ajax DOT org>
+ * @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License
+ */
+
+var jsDAV = require("./../../jsdav"),
+ jsDAV_iNode = require("./../iNode").jsDAV_iNode,
+
+ Fs = require("fs"),
+ Path = require("path"),
+ Util = require("./../util"),
+ Exc = require("./../exceptions");
+
+function jsDAV_SFTP_Node(path, sftp) {
+ this.path = (path || "").replace(/[\/]+$/, "");
+ this.sftp = sftp;
+}
+
+exports.jsDAV_SFTP_Node = jsDAV_SFTP_Node;
+
+(function() {
+ /**
+ * Returns the name of the node
+ *
+ * @return {string}
+ */
+ this.getName = function() {
+ return Util.splitPath(this.path)[1];
+ };
+
+ /**
+ * Renames the node
+ *
+ * @param {string} name The new name
+ * @return void
+ */
+ this.setName = function(name, cbfssetname) {
+ var parentPath = Util.splitPath(this.path)[0],
+ newName = Util.splitPath(name)[1];
+
+ var newPath = parentPath + "/" + newName;
+ var _self = this;
+ this.sftp.rename(this.path, newPath, function(err) {
+ if (err)
+ return cbfssetname(err);
+ _self.path = newPath;
+ cbfssetname();
+ });
+ };
+
+ /**
+ * Returns the last modification time, as a unix timestamp
+ *
+ * @return {Number}
+ */
+ this.getLastModified = function(cbfsgetlm) {
+ if (this.$stat)
+ return cbfsgetlm(null, this.$stat.mtime);
+ var _self = this;
+ this.sftp.stat(this.path, function(err, stat) {
+ if (err || typeof stat == "undefined")
+ return cbfsgetlm(err);
+ //_self.$stat = stat;
+ cbfsgetlm(null, stat.mtime);
+ });
+ };
+
+ /**
+ * Returns whether a node exists or not
+ *
+ * @return {Boolean}
+ */
+ this.exists = function(cbfsexist) {
+ this.sftp.stat(this.path, function(err, stat) {
+ cbfsexist(Boolean(!err && stat))
+ });
+ };
+}).call(jsDAV_SFTP_Node.prototype = new jsDAV_iNode());
View
133 lib/DAV/tree/sftp.js
@@ -0,0 +1,133 @@
+/*
+ * @package jsDAV
+ * @subpackage DAV
+ * @copyright Copyright (C) 2010 Mike de Boer. All rights reserved.
+ * @author Mike de Boer <mike AT ajax DOT org>
+ * @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License
+ */
+
+var jsDAV_Tree = require("./../tree").jsDAV_Tree,
+ jsDAV_SFTP_Directory = require("./../sftp/directory").jsDAV_SFTP_Directory,
+ jsDAV_SFTP_File = require("./../sftp/file").jsDAV_SFTP_File,
+
+ Fs = require("fs"),
+ Sftp = require("./../../../support/node-sftp"),
+ Async = require("./../../../support/async.js"),
+ Util = require("./../util"),
+ Exc = require("./../exceptions");
+
+/**
+ * jsDAV_Tree_Sftp
+ *
+ * Creates this tree
+ * Supply the path you'd like to share.
+ *
+ * @param {String} basePath
+ * @contructor
+ */
+function jsDAV_Tree_Sftp(options) {
+ this.basePath = (options.sftp && options.sftp.home) || "";
+ this.sftp = new Sftp(options.sftp || {}, function(err) {
+ // throw it anyway, because it's fatal...
+ if (err)
+ throw err;
+ });
+ Util.EventEmitter.DEFAULT_TIMEOUT = 10000;
+ var _self = this;
+ process.on("exit", function() {
+ _self.sftp.disconnect();
+ });
+}
+
+exports.jsDAV_Tree_Sftp = jsDAV_Tree_Sftp;
+
+(function() {
+ /**
+ * Disconnect from an open Sftp session to not have child processes hanging
+ * around in zombie mode.
+ *
+ * @return void
+ */
+ this.unmount = function() {
+ this.sftp.disconnect();
+ };
+
+ /**
+ * Returns a new node for the given path
+ *
+ * @param string path
+ * @return void
+ */
+ this.getNodeForPath = function(path, cbfstree) {
+ var realPath = this.getRealPath(path),
+ _self = this;
+ this.sftp.stat(realPath, function(err, stat) {
+ if (!Util.empty(err))
+ return cbfstree(new Exc.jsDAV_Exception_FileNotFound("File at location " + realPath + " not found"));
+ cbfstree(null, stat.isDirectory()
+ ? new jsDAV_SFTP_Directory(realPath, _self.sftp)
+ : new jsDAV_SFTP_File(realPath, _self.sftp))
+ });
+ };
+
+ /**
+ * Returns the real filesystem path for a webdav url.
+ *
+ * @param string publicPath
+ * @return string
+ */
+ this.getRealPath = function(publicPath) {
+ return (Util.rtrim(this.basePath, "/") + "/" + Util.trim(publicPath, "/")).replace(/[\/]+$/, "");
+ };
+
+ /**
+ * Copies a file or directory.
+ *
+ * This method must work recursively and delete the destination
+ * if it exists
+ *
+ * @param string source
+ * @param string destination
+ * @return void
+ */
+ this.copy = function(source, destination, cbfscopy) {
+ //@TODO!!!!
+ source = this.getRealPath(source);
+ destination = this.getRealPath(destination);
+ this.realCopy(source, destination, cbfscopy);
+ };
+
+ /**
+ * Used by self::copy
+ *
+ * @param string source
+ * @param string destination
+ * @return void
+ */
+ this.realCopy = function(source, destination, cbfsrcopy) {
+ //@TODO!!!!
+ this.sftp.stat(source, function(err, stat) {
+ if (!Util.empty(err))
+ return cbfsrcopy(err);
+ if (stat.isFile())
+ Async.copyfile(source, destination, true, cbfsrcopy);
+ else
+ Async.copytree(source, destination, cbfsrcopy);
+ });
+ };
+
+ /**
+ * Moves a file or directory recursively.
+ *
+ * If the destination exists, delete it first.
+ *
+ * @param string source
+ * @param string destination
+ * @return void
+ */
+ this.move = function(source, destination, cbfsmove) {
+ source = this.getRealPath(source);
+ destination = this.getRealPath(destination);
+ this.sftp.rename(source, destination, cbfsmove);
+ };
+}).call(jsDAV_Tree_Sftp.prototype = new jsDAV_Tree());
View
2  lib/DAV/util.js
@@ -938,7 +938,7 @@ exports.EventEmitter.DEFAULT_TIMEOUT = 2000; // in milliseconds
}
}).end(function(err) {
if (jsDAV.debugMode && err)
- console.log("argument after event: " + err);
+ console.log("argument after event '" + eventName + "': " + err);
cbdispatch(err);
});
};
View
21 lib/jsdav.js
@@ -74,10 +74,27 @@ exports.createServer = function(options, port, host) {
return DAV.createServer(options, port, host);
};
-exports.mount = function(path, mountpoint, server, standalone) {
+/**
+ * Create a jsDAV Server object that will not fire up listening to HTTP requests,
+ * but instead will respond to requests that are passed to
+ * 1) the custom NodeJS httpServer provided by the 'server' option or
+ * 2) the Server.handle() function.
+ *
+ * @param {Object} options Options to be passed to the jsDAV Server object, which
+ * should look like:
+ * [code]
+ * {
+ * node : path,
+ * mount : mountpoint,
+ * server : server,
+ * standalone: standalone
+ * }
+ * [/code]
+ */
+exports.mount = function(options) {
var DAV = require("./DAV/server");
DAV.debugMode = exports.debugMode;
- return DAV.mount(path, mountpoint, server, standalone);
+ return DAV.mount(options);
};
//@todo implement CalDAV
View
6 test/test_mount.js
@@ -18,4 +18,8 @@ var server = Http.createServer(function(req, resp) {
server.listen(8080, "127.0.0.1");
-jsDAV.mount(__dirname + "/assets", "test", server);
+jsDAV.mount({
+ path: __dirname + "/assets",
+ mount: "test",
+ server: server
+});
View
8 test/test_server.js
@@ -6,8 +6,12 @@
* @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License
*/
-var jsDAV = require("./../lib/jsdav");
+var jsDAV = require("./../lib/jsdav"),
+ jsDAV_Locks_Backend_FS = require("./../lib/DAV/plugins/locks/fs");
jsDAV.debugMode = true;
-jsDAV.createServer({node: __dirname + "/assets"}, 8000);
+jsDAV.createServer({
+ node: __dirname + "/assets"/*,
+ locksBackend: new jsDAV_Locks_Backend_FS(__dirname + "/assets")*/
+}, 8000);
View
53 test/test_sftp.js
@@ -0,0 +1,53 @@
+/*
+ * @package jsDAV
+ * @subpackage DAV
+ * @copyright Copyright (C) 2010 Mike de Boer. All rights reserved.
+ * @author Mike de Boer <mike AT ajax DOT org>
+ * @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License
+ */
+
+var jsDAV = require("./../lib/jsdav");
+
+jsDAV.debugMode = true;
+
+var prvkey = "-----BEGIN RSA PRIVATE KEY-----\n\
+MIIEpQIBAAKCAQEAw0hN+bMuhMuHOOzakpmuf8OS6ieHVc7D8b0elXQZIptEOln2\n\
+vwr506E69iqmh7UM6wbGPZSqlAEyqYq9zwkHKzFoJuHKtv/IDE5EcdV8DLR/+l1Q\n\
+c+pnHFc4iZOdO/cG4qnldeiHMu1R2MWG2MgpO3/WH4HsWmwEZkjG7SYbbStQXaSg\n\
+zDkitpKIt6BjSCjTKnVb3DadBGuQpx29lKvN86n7sH4wEGgkhifZoV77V3+T/1Fu\n\
+nrgNxyVgz6/DNekP6vAcsR8x59ujUnHpPAKAHGCLFizlwt2OLwf2p//GAGS1Zgf9\n\
+JpRhZAqCxDMz9y5bC/mp02NiRWPZtDd3nCzaRQIDAQABAoIBAQCyZXVGbVhL3Bq1\n\
++DpcvqRY93NZEa9ixjbeueQcqCjmIm2b2N+++unrWVkh1Si4xL7+Xfvv+cYy2z1L\n\
+AQIRBrBT1xjMnGyx7Mz14PJKA7sFaEeZknGS00pK66ssk3uKcksJ+iczJa+M6Jxi\n\
+qWBc3c49GrWjpu8iU5dZUZbYwn0/pjvu+pyb4olh5aIWyMiMPdPZBIXfVUMVb8NT\n\
+y0LesnQH2RtOw7rY2fvb02djl+TvKstbAKERFigY2TQvyh8Jp3a3HUWIDKClEJkD\n\
+cSaZt7peqWi9t3k8Ibu7elTk2yR5eEUjQyFyIblVaI77CXBjGXCQzk2wvNnr3NKX\n\
+3jlm6gBpAoGBAP9WYGTmz1bIgSXxzsesmv1rrfiQ+lDwZukDYStG+zH82qdnsxf/\n\
+r1SHmynWTfYz279vjPkWF1pFjX2dpj2Wm1LvrS5A6E/JbqSTtoyICdA+A+/TPh48\n\
+iNSHmt2p+BUW9Q2PpRNUYqk6z2PJIyniWCBCTHXyFOLaLe4zdRU5T26fAoGBAMPK\n\
+CGpbNdR/P6A1IEd+5ShaRGmLwSYJWbMpLWbk93eDyE/P8UnM61EV5Ae8f0boNKdk\n\
+Ot4vHmQzVGRKRZhi0p+/rkEnpIGyqr9tSIKraNyEJir6r4jIChFqpdZvxziv6cPa\n\
++BJpTyYMMqT7SIBRMCU13Mqpfq9Fnzvyh6CqHyCbAoGBAI9tKJZlJEBePlVfH8T/\n\
+iswhSUbfwQvoDhaDZHiX1ZA9tWDlmi8323fC+ICmtYI/nQdKlMhyBUoa2aCfBnt/\n\
+9t2+bewWX6g5wOHHa3pDDCgiPbngUftQC5g+V9p9mDHYhGxKrPJPq1/d/hLSL+Ne\n\
+FhyAwUxbYCoRXk14MCNs3taHAoGBAI9821oG6paHg3vIM5XyO8OtFAI+OBnGNIUH\n\
+Io0MNQjT/dPwU6eAlNziLDI3RRgUSbJ71GDNK3rH24t8mzCpDC+jbPO3N+sNo/GT\n\
+B9csBDfIaaiJ/GdEI4zMGinj1Z+H3Mx7B9+Gakk6G0uqFWJlHeHHbb7hJUUSwzZN\n\
+8nQe+Z0NAoGAbSPinZEZVgBn2t8nNgU4The+l7KyQT8/bPT0C+PAHxLmnw/+xKRQ\n\
+4978TJp/72fpFh8n9b4rosSjxFk2mxXZlM16eyOHZXpBT21agU9NbaJ4SEHj/5Ij\n\
+ZFfOuDr1lUZW0pBL3lDt+kjkrx29K4WNMr7e7RJPv3vGwtyM75x6eRo=\n\
+-----END RSA PRIVATE KEY-----";
+var pubkey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDSE35sy6Ey4c47NqSma5/w5LqJ4dVzsPxvR6VdBkim0Q6Wfa/CvnToTr2KqaHtQzrBsY9lKqUATKpir3PCQcrMWgm4cq2/8gMTkRx1XwMtH/6XVBz6mccVziJk5079wbiqeV16Icy7VHYxYbYyCk7f9YfgexabARmSMbtJhttK1BdpKDMOSK2koi3oGNIKNMqdVvcNp0Ea5CnHb2Uq83zqfuwfjAQaCSGJ9mhXvtXf5P/UW6euA3HJWDPr8M16Q/q8ByxHzHn26NScek8AoAcYIsWLOXC3Y4vB/an/8YAZLVmB/0mlGFkCoLEMzP3LlsL+anTY2JFY9m0N3ecLNpF cloud9@vps6782.xlshosting.net";
+
+var host = "stage.io";
+var username = "sshtest";
+
+jsDAV.createServer({
+ type: "sftp",
+ sftp: {
+ host: host,
+ privateKey: prvkey,
+ username: username,
+ home: "/home/sshtest"
+ }
+}, 8000);
Please sign in to comment.
Something went wrong with that request. Please try again.