From 3ffaded8094125a9b9354ea91419421c2c9a965c Mon Sep 17 00:00:00 2001
From: Brian Warner <warner@allmydata.com>
Date: Mon, 6 Oct 2008 21:36:18 -0700
Subject: [PATCH] web: change t=manifest to return a list of
 (path,read/writecap) tuples, instead of a list of verifycaps. Add
 output=html,text,json.

---
 docs/webapi.txt                    | 12 +++++++++-
 src/allmydata/dirnode.py           | 14 ++++-------
 src/allmydata/interfaces.py        |  7 +++---
 src/allmydata/test/test_dirnode.py |  6 ++---
 src/allmydata/test/test_system.py  |  5 ++--
 src/allmydata/test/test_web.py     | 31 +++++++++++++++++++++----
 src/allmydata/web/directory.py     | 37 +++++++++++++++++++++++++++---
 src/allmydata/web/info.py          |  7 ++++++
 src/allmydata/web/manifest.xhtml   |  6 +++--
 9 files changed, 98 insertions(+), 27 deletions(-)

diff --git a/docs/webapi.txt b/docs/webapi.txt
index a02b3af8..62705f21 100644
--- a/docs/webapi.txt
+++ b/docs/webapi.txt
@@ -871,7 +871,17 @@ POST $URL?t=deep-check&repair=true
 GET $DIRURL?t=manifest
 
   Return an HTML-formatted manifest of the given directory, for debugging.
-  This is a table of verifier-caps.
+  This is a table of (path, filecap/dircap), for every object reachable from
+  the starting directory. The path will be slash-joined, and the
+  filecap/dircap will contain a link to the object in question. This page
+  gives immediate access to every object in the virtual filesystem subtree.
+
+  If output=text is added to the query args, the results will be a text/plain
+  list, with one file/dir per line, slash-separated, with the filecap/dircap
+  separated by a space.
+
+  If output=JSON is added to the queryargs, then the results will be a
+  JSON-formatted list of (path, cap) tuples, where path is a list of strings.
 
 GET $DIRURL?t=deep-size
 
diff --git a/src/allmydata/dirnode.py b/src/allmydata/dirnode.py
index a69e9996..3cc14167 100644
--- a/src/allmydata/dirnode.py
+++ b/src/allmydata/dirnode.py
@@ -503,8 +503,8 @@ class NewDirectoryNode:
 
 
     def build_manifest(self):
-        """Return a frozenset of verifier-capability strings for all nodes
-        (directories and files) reachable from this one."""
+        """Return a list of (path, cap) tuples, for all nodes (directories
+        and files) reachable from this one."""
         return self.deep_traverse(ManifestWalker())
 
     def deep_stats(self):
@@ -521,17 +521,13 @@ class NewDirectoryNode:
 
 class ManifestWalker:
     def __init__(self):
-        self.manifest = set()
+        self.manifest = []
     def add_node(self, node, path):
-        v = node.get_verifier()
-        # LIT files have no verify-cap, so don't add them
-        if v:
-            assert not isinstance(v, str), "ICK: %s %s" % (v, node)
-            self.manifest.add(v.to_string())
+        self.manifest.append( (tuple(path), node.get_uri()) )
     def enter_directory(self, parent, children):
         pass
     def finish(self):
-        return frozenset(self.manifest)
+        return self.manifest
 
 
 class DeepStats:
diff --git a/src/allmydata/interfaces.py b/src/allmydata/interfaces.py
index 9a0f3d3f..b508fb21 100644
--- a/src/allmydata/interfaces.py
+++ b/src/allmydata/interfaces.py
@@ -793,9 +793,10 @@ class IDirectoryNode(IMutableFilesystemNode):
         operation finishes. The child name must be a unicode string."""
 
     def build_manifest():
-        """Return a Deferred that fires with a frozenset of
-        verifier-capability strings for all nodes (directories and files)
-        reachable from this one."""
+        """Return a Deferred that fires with a list of (path, cap) tuples for
+        nodes (directories and files) reachable from this one. 'path' will be
+        a tuple of unicode strings. The origin dirnode will be represented by
+        an empty path tuple."""
 
     def deep_stats():
         """Return a Deferred that fires with a dictionary of statistics
diff --git a/src/allmydata/test/test_dirnode.py b/src/allmydata/test/test_dirnode.py
index 4858b963..e90c5859 100644
--- a/src/allmydata/test/test_dirnode.py
+++ b/src/allmydata/test/test_dirnode.py
@@ -280,7 +280,7 @@ class Dirnode(unittest.TestCase, testutil.ShouldFailMixin, testutil.StallMixin):
             self.failUnless(u_ro.startswith("URI:DIR2-RO:"), u_ro)
             u_v = n.get_verifier().to_string()
             self.failUnless(u_v.startswith("URI:DIR2-Verifier:"), u_v)
-            self.expected_manifest.append(u_v)
+            self.expected_manifest.append( ((), u) )
             expected_si = n._uri._filenode_uri.storage_index
             self.failUnlessEqual(n.get_storage_index(), expected_si)
 
@@ -292,7 +292,7 @@ class Dirnode(unittest.TestCase, testutil.ShouldFailMixin, testutil.StallMixin):
             other_file_uri = make_mutable_file_uri()
             m = Marker(fake_file_uri)
             ffu_v = m.get_verifier().to_string()
-            self.expected_manifest.append(ffu_v)
+            self.expected_manifest.append( ((u"child",) , m.get_uri()) )
             d.addCallback(lambda res: n.set_uri(u"child", fake_file_uri))
             d.addCallback(lambda res:
                           self.shouldFail(ExistingChildError, "set_uri-no",
@@ -312,7 +312,7 @@ class Dirnode(unittest.TestCase, testutil.ShouldFailMixin, testutil.StallMixin):
                 self.subdir = subdir
                 new_v = subdir.get_verifier().to_string()
                 assert isinstance(new_v, str)
-                self.expected_manifest.append(new_v)
+                self.expected_manifest.append( ((u"subdir",), subdir.get_uri()) )
             d.addCallback(_created)
 
             d.addCallback(lambda res:
diff --git a/src/allmydata/test/test_system.py b/src/allmydata/test/test_system.py
index ad614c38..19c90573 100644
--- a/src/allmydata/test/test_system.py
+++ b/src/allmydata/test/test_system.py
@@ -928,13 +928,14 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
 
             d1.addCallback(lambda res: home.build_manifest())
             d1.addCallback(self.log, "manifest")
-            #  four items:
+            #  five items:
+            # P/
             # P/personal/
             # P/personal/sekrit data
             # P/s2-rw  (same as P/s2-ro)
             # P/s2-rw/mydata992 (same as P/s2-rw/mydata992)
             d1.addCallback(lambda manifest:
-                           self.failUnlessEqual(len(manifest), 4))
+                           self.failUnlessEqual(len(manifest), 5))
             d1.addCallback(lambda res: home.deep_stats())
             def _check_stats(stats):
                 expected = {"count-immutable-files": 1,
diff --git a/src/allmydata/test/test_web.py b/src/allmydata/test/test_web.py
index 05957712..4cf7b59e 100644
--- a/src/allmydata/test/test_web.py
+++ b/src/allmydata/test/test_web.py
@@ -831,10 +831,33 @@ class Web(WebMixin, unittest.TestCase):
         return d
 
     def test_GET_DIRURL_manifest(self):
-        d = self.GET(self.public_url + "/foo?t=manifest", followRedirect=True)
-        def _got(manifest):
-            self.failUnless("Refresh Capabilities" in manifest)
-        d.addCallback(_got)
+        def getman(ignored, suffix, followRedirect=False):
+            return self.GET(self.public_url + "/foo" + suffix,
+                            followRedirect=followRedirect)
+        d = defer.succeed(None)
+        d.addCallback(getman, "?t=manifest", followRedirect=True)
+        def _got_html(manifest):
+            self.failUnless("Manifest of SI=" in manifest)
+            self.failUnless("<td>sub</td>" in manifest)
+            self.failUnless(self._sub_uri in manifest)
+            self.failUnless("<td>sub/baz.txt</td>" in manifest)
+        d.addCallback(_got_html)
+        d.addCallback(getman, "/?t=manifest")
+        d.addCallback(_got_html)
+        d.addCallback(getman, "/?t=manifest&output=text")
+        def _got_text(manifest):
+            self.failUnless("\nsub " + self._sub_uri + "\n" in manifest)
+            self.failUnless("\nsub/baz.txt URI:CHK:" in manifest)
+        d.addCallback(_got_text)
+        d.addCallback(getman, "/?t=manifest&output=JSON")
+        def _got_json(manifest):
+            data = simplejson.loads(manifest)
+            got = {}
+            for (path_list, cap) in data:
+                got[tuple(path_list)] = cap
+            self.failUnlessEqual(got[(u"sub",)], self._sub_uri)
+            self.failUnless((u"sub",u"baz.txt") in got)
+        d.addCallback(_got_json)
         return d
 
     def test_GET_DIRURL_deepsize(self):
diff --git a/src/allmydata/web/directory.py b/src/allmydata/web/directory.py
index 359f0521..075c6d0a 100644
--- a/src/allmydata/web/directory.py
+++ b/src/allmydata/web/directory.py
@@ -6,7 +6,7 @@ import time
 from twisted.internet import defer
 from twisted.python.failure import Failure
 from twisted.web import http, html
-from nevow import url, rend, tags as T
+from nevow import url, rend, inevow, tags as T
 from nevow.inevow import IRequest
 
 from foolscap.eventual import fireEventually
@@ -676,6 +676,36 @@ class RenameForm(rend.Page):
 class Manifest(rend.Page):
     docFactory = getxmlfile("manifest.xhtml")
 
+    def renderHTTP(self, ctx):
+        output = get_arg(inevow.IRequest(ctx), "output", "html").lower()
+        if output == "text":
+            return self.text(ctx)
+        if output == "json":
+            return self.json(ctx)
+        return rend.Page.renderHTTP(self, ctx)
+
+    def slashify_path(self, path):
+        if not path:
+            return ""
+        return "/".join([p.encode("utf-8") for p in path])
+
+    def text(self, ctx):
+        inevow.IRequest(ctx).setHeader("content-type", "text/plain")
+        d = self.original.build_manifest()
+        def _render_text(manifest):
+            lines = []
+            for (path, cap) in manifest:
+                lines.append(self.slashify_path(path) + " " + cap)
+            return "\n".join(lines) + "\n"
+        d.addCallback(_render_text)
+        return d
+
+    def json(self, ctx):
+        inevow.IRequest(ctx).setHeader("content-type", "text/plain")
+        d = self.original.build_manifest()
+        d.addCallback(lambda manifest: simplejson.dumps(manifest))
+        return d
+
     def render_title(self, ctx):
         return T.title["Manifest of SI=%s" % abbreviated_dirnode(self.original)]
 
@@ -685,8 +715,9 @@ class Manifest(rend.Page):
     def data_items(self, ctx, data):
         return self.original.build_manifest()
 
-    def render_row(self, ctx, refresh_cap):
-        ctx.fillSlots("refresh_capability", refresh_cap)
+    def render_row(self, ctx, (path, cap)):
+        ctx.fillSlots("path", self.slashify_path(path))
+        ctx.fillSlots("cap", cap)
         return ctx.tag
 
 def DeepSize(ctx, dirnode):
diff --git a/src/allmydata/web/info.py b/src/allmydata/web/info.py
index dd0c3daf..324698c1 100644
--- a/src/allmydata/web/info.py
+++ b/src/allmydata/web/info.py
@@ -230,6 +230,13 @@ class MoreInfo(rend.Page):
             T.fieldset[
             T.input(type="hidden", name="t", value="manifest"),
             T.legend(class_="freeform-form-label")["Run a manifest operation (EXPENSIVE)"],
+            T.div["Output Format: ",
+                  T.select(name="output")
+                  [ T.option(value="html", selected="true")["HTML"],
+                    T.option(value="text")["text"],
+                    T.option(value="json")["JSON"],
+                    ],
+                  ],
             T.input(type="submit", value="Manifest"),
             ]]
         return ctx.tag[manifest]
diff --git a/src/allmydata/web/manifest.xhtml b/src/allmydata/web/manifest.xhtml
index 0e57fe6d..6dff70f5 100644
--- a/src/allmydata/web/manifest.xhtml
+++ b/src/allmydata/web/manifest.xhtml
@@ -13,10 +13,12 @@
 
 <table n:render="sequence" n:data="items" border="1">
   <tr n:pattern="header">
-    <td>Refresh Capabilities</td>
+    <td>Path</td>
+    <td>cap</td>
   </tr>
   <tr n:pattern="item" n:render="row">
-    <td><n:slot name="refresh_capability"/></td>
+    <td><n:slot name="path"/></td>
+    <td><n:slot name="cap"/></td>
   </tr>
 
   <tr n:pattern="empty"><td>no items in the manifest!</td></tr>
-- 
2.45.2