diff --git a/pkg/server/ui.go b/pkg/server/ui.go
index 09e245bc0..3687730c3 100644
--- a/pkg/server/ui.go
+++ b/pkg/server/ui.go
@@ -428,6 +428,11 @@ func (ui *UIHandler) serveNewUI(rw http.ResponseWriter, req *http.Request) {
serveStaticFile(rw, req, newuiFiles, file)
return
}
+ if wantsFileTreePage(req) {
+ file := "/filetree.html"
+ serveStaticFile(rw, req, newuiFiles, file)
+ return
+ }
if suffix == "new" {
// Add a trailing slash.
diff --git a/server/camlistored/newui/filetree.css b/server/camlistored/newui/filetree.css
new file mode 100644
index 000000000..5c4b05b14
--- /dev/null
+++ b/server/camlistored/newui/filetree.css
@@ -0,0 +1,32 @@
+/*
+Copyright 2011 The Camlistore Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.cam-filetree-page {
+ font: 16px/1.4 normal Arial, sans-serif;
+}
+.cam-filetree-nav:before {
+ content: "[";
+}
+.cam-filetree-nav:after {
+ content: "]";
+}
+.cam-filetree-newp {
+ text-decoration: underline;
+ cursor: pointer;
+ color: darkgreen;
+ margin-left: .4em;
+ font-size: 80%;
+}
diff --git a/server/camlistored/newui/filetree.html b/server/camlistored/newui/filetree.html
new file mode 100644
index 000000000..a1deaccab
--- /dev/null
+++ b/server/camlistored/newui/filetree.html
@@ -0,0 +1,28 @@
+
+
+
+ Filetree
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FileTree for
+
+
+
+
+
diff --git a/server/camlistored/newui/filetree.js b/server/camlistored/newui/filetree.js
new file mode 100644
index 000000000..faae960be
--- /dev/null
+++ b/server/camlistored/newui/filetree.js
@@ -0,0 +1,281 @@
+/*
+Copyright 2011 The Camlistore Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+/**
+ * @fileoverview Filetree page.
+ *
+ */
+goog.provide('camlistore.FiletreePage');
+
+goog.require('goog.dom');
+goog.require('goog.events.EventType');
+goog.require('goog.ui.Component');
+goog.require('camlistore.ServerConnection');
+
+/**
+ * @param {camlistore.ServerType.DiscoveryDocument} config Global config
+ * of the current server this page is being rendered for.
+ * @param {goog.dom.DomHelper=} opt_domHelper DOM helper to use.
+ *
+ * @extends {goog.ui.Component}
+ * @constructor
+ */
+camlistore.FiletreePage = function(config, opt_domHelper) {
+ goog.base(this, opt_domHelper);
+
+ /**
+ * @type {Object}
+ * @private
+ */
+ this.config_ = config;
+
+ /**
+ * @type {camlistore.ServerConnection}
+ * @private
+ */
+ this.connection_ = new camlistore.ServerConnection(config);
+
+};
+goog.inherits(camlistore.FiletreePage, goog.ui.Component);
+
+
+/**
+ * @type {number}
+ * @private
+ */
+camlistore.FiletreePage.prototype.indentStep_ = 20;
+
+
+function getPermanodeParam() {
+ var blobRef = getQueryParam('d');
+ return (blobRef && isPlausibleBlobRef(blobRef)) ? blobRef : null;
+}
+
+// Returns the first value from the query string corresponding to |key|.
+// Returns null if the key isn't present.
+getQueryParam = function(key) {
+ var params = document.location.search.substring(1).split('&');
+ for (var i = 0; i < params.length; ++i) {
+ var parts = params[i].split('=');
+ if (parts.length == 2 && decodeURIComponent(parts[0]) == key)
+ return decodeURIComponent(parts[1]);
+ }
+ return null;
+};
+
+// Returns true if the passed-in string might be a blobref.
+isPlausibleBlobRef = function(blobRef) {
+ return /^\w+-[a-f0-9]+$/.test(blobRef);
+};
+
+
+/**
+ * Called when component's element is known to be in the document.
+ */
+camlistore.FiletreePage.prototype.enterDocument = function() {
+ camlistore.FiletreePage.superClass_.enterDocument.call(this);
+ var blobref = getPermanodeParam();
+
+ if (blobref) {
+ this.connection_.describeWithThumbnails(
+ blobref,
+ 0,
+ goog.bind(this.handleDescribeBlob_, this, blobref),
+ function(msg) {
+ alert("failed to get blob description: " + msg);
+ }
+ );
+ }
+}
+
+/**
+ * @param {string} permanode Node to describe.
+ * @param {camlistore.ServerType.DescribeResponse} describeResult Object of properties for the node.
+ * @private
+ */
+camlistore.FiletreePage.prototype.handleDescribeBlob_ =
+function(permanode, describeResult) {
+ var meta = describeResult.meta;
+ if (!meta[permanode]) {
+ alert("didn't get blob " + permanode);
+ return;
+ }
+ var binfo = meta[permanode];
+ if (!binfo) {
+ alert("Error describing blob " + permanode);
+ return;
+ }
+ if (binfo.camliType != "directory") {
+ alert("Does not contain a directory");
+ return;
+ }
+ this.connection_.getBlobContents(
+ permanode,
+ goog.bind(function(data) {
+ var finfo = JSON.parse(data);
+ var fileName = finfo.fileName;
+ var curDir = document.getElementById('curDir');
+ curDir.innerHTML = "" + fileName + " ";
+ this.buildTree_();
+ }, this),
+ function(msg) {
+ alert("failed to get blobcontents: " + msg);
+ }
+ );
+}
+
+/**
+ * @private
+ */
+camlistore.FiletreePage.prototype.buildTree_ = function() {
+ var blobref = getPermanodeParam();
+ var children = goog.dom.getElement("children");
+ this.connection_.getFileTree(blobref,
+ goog.bind(function(jres) {
+ this.onChildrenFound_(children, 0, jres);
+ }, this)
+ );
+}
+
+/**
+ * @param {string} div node used as root for the tree
+ * @param {number} depth how deep we are in the tree, for indenting
+ * @param {camlistore.ServerType.DescribeResponse} jres describe result
+ * @private
+ */
+camlistore.FiletreePage.prototype.onChildrenFound_ =
+function(div, depth, jres) {
+ var indent = depth * camlistore.FiletreePage.prototype.indentStep_;
+ div.innerHTML = "";
+ for (var i = 0; i < jres.children.length; i++) {
+ var children = jres.children;
+ var pdiv = goog.dom.createElement("div");
+ var alink = goog.dom.createElement("a");
+ alink.style.paddingLeft=indent + "px"
+ alink.id = children[i].blobRef;
+ switch (children[i].type) {
+ case 'directory':
+ goog.dom.setTextContent(alink, "+ " + children[i].name);
+ goog.events.listen(alink,
+ goog.events.EventType.CLICK,
+ goog.bind(function (p, d) {
+ this.unFold_(p, d);
+ }, this, alink.id, depth),
+ false, this
+ );
+ break;
+ case 'file':
+ goog.dom.setTextContent(alink, " " + children[i].name);
+ alink.href = "./?b=" + alink.id;
+ break;
+ default:
+ alert("not a file or dir");
+ break;
+ }
+ var newPerm = goog.dom.createElement("span");
+ newPerm.className = "cam-filetree-newp";
+ goog.dom.setTextContent(newPerm, "P");
+ goog.events.listen(newPerm,
+ goog.events.EventType.CLICK,
+ this.newPermWithContent_(alink.id),
+ false, this
+ );
+ goog.dom.appendChild(pdiv, alink);
+ goog.dom.appendChild(pdiv, newPerm);
+ goog.dom.appendChild(div, pdiv);
+ }
+}
+
+
+/**
+ * @param {string} content blobref of the content
+ * @private
+ */
+camlistore.FiletreePage.prototype.newPermWithContent_ =
+function(content) {
+ var fun = function(e) {
+ this.connection_.createPermanode(
+ goog.bind(function(permanode) {
+ this.connection_.newAddAttributeClaim(
+ permanode, "camliContent", content,
+ function() {
+ alert("permanode created");
+ },
+ function(msg) {
+ // TODO(mpl): "cancel" new permanode
+ alert("set permanode content failed: " + msg);
+ }
+ );
+ }, this),
+ function(msg) {
+ alert("create permanode failed: " + msg);
+ }
+ );
+ }
+ return goog.bind(fun, this);
+}
+
+
+/**
+ * @param {string} blobref dir to unfold.
+ * @param {number} depth so we know how much to indent.
+ * @private
+ */
+camlistore.FiletreePage.prototype.unFold_ =
+function(blobref, depth) {
+ var node = goog.dom.getElement(blobref);
+ var div = goog.dom.createElement("div");
+ this.connection_.getFileTree(blobref,
+ goog.bind(function(jres) {
+ this.onChildrenFound_(div, depth+1, jres);
+ insertAfter(node, div);
+ goog.events.removeAll(node);
+ goog.events.listen(node,
+ goog.events.EventType.CLICK,
+ goog.bind(function(p, d) {
+ this.fold_(p, d);
+ }, this, blobref, depth),
+ false, this
+ );
+ }, this)
+ );
+}
+
+function insertAfter( referenceNode, newNode ) {
+ // nextSibling X2 because of the "P" span
+ referenceNode.parentNode.insertBefore( newNode, referenceNode.nextSibling.nextSibling );
+}
+
+/**
+ * @param {string} nodeid id of the node to fold.
+ * @param {depth} depth so we know how much to indent.
+ * @private
+ */
+camlistore.FiletreePage.prototype.fold_ =
+function(nodeid, depth) {
+ var node = goog.dom.getElement(nodeid);
+ // nextSibling X2 because of the "P" span
+ node.parentNode.removeChild(node.nextSibling.nextSibling);
+ goog.events.removeAll(node);
+ goog.events.listen(node,
+ goog.events.EventType.CLICK,
+ goog.bind(function(p, d) {
+ this.unFold_(p, d);
+ }, this, nodeid, depth),
+ false, this
+ );
+}
+
diff --git a/server/camlistored/newui/server_connection.js b/server/camlistored/newui/server_connection.js
index c973239e8..5916cd5a1 100644
--- a/server/camlistored/newui/server_connection.js
+++ b/server/camlistored/newui/server_connection.js
@@ -153,6 +153,27 @@ function(success, opt_fail, e) {
this.handleXhrResponseJson_(success, this.safeFail_(opt_fail), e);
};
+/**
+ * @param {string} blobref root of the tree
+ * @param {function} success callback with data.
+ * @param {?Function} opt_fail optional failure calback
+ */
+camlistore.ServerConnection.prototype.getFileTree =
+function(blobref, success, opt_fail) {
+
+ // TODO(mpl): fix when we do the switch to newui. and
+ // redo it relatively to one of the roots anyway?
+ var path = "../tree/" + blobref;
+
+ this.sendXhr_(
+ path,
+ goog.bind(this.genericHandleSearch_, this,
+ success, this.safeFail_(opt_fail)
+ )
+ );
+};
+
+
/**
* @param {function(camlistore.ServerType.SearchRecentResponse)} success callback with data.
* @param {number=} opt_thumbnailSize